Skip to content

Commit 6fbd3bf

Browse files
authored
Merge pull request #1387 from samay2504/fix/modernize-dynamic-imports
refactor: modernize dynamic imports with importlib and stdlib cached_property (fixes issue #1386)
2 parents f99b0df + d9c1a5e commit 6fbd3bf

File tree

2 files changed

+63
-67
lines changed

2 files changed

+63
-67
lines changed

pysal/base.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22
Base information for pysal meta package
33
"""
44

5+
import importlib
6+
from functools import cached_property
57

68
federation_hierarchy = {
7-
'explore': ['esda', 'giddy', 'segregation',
8-
'pointpats', 'inequality',
9-
'spaghetti', 'access', 'momepy'],
10-
'model': ['spreg', 'spglm', 'tobler', 'spint',
11-
'mgwr', 'access', 'spopt'],
12-
'viz': ['splot', 'mapclassify'],
13-
'lib': ['libpysal']
9+
"explore": [
10+
"esda",
11+
"giddy",
12+
"segregation",
13+
"pointpats",
14+
"inequality",
15+
"spaghetti",
16+
"access",
17+
"momepy",
18+
],
19+
"model": ["spreg", "spglm", "tobler", "spint", "mgwr", "access", "spopt"],
20+
"viz": ["splot", "mapclassify"],
21+
"lib": ["libpysal"],
1422
}
1523

1624
memberships = {}
@@ -19,44 +27,36 @@
1927
memberships[package] = key
2028

2129

22-
23-
class cached_property(object):
24-
""" A property that is only computed once per instance and then replaces
25-
itself with an ordinary attribute. Deleting the attribute resets the
26-
property.
27-
28-
Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
29-
"""
30-
31-
def __init__(self, func):
32-
self.__doc__ = getattr(func, '__doc__')
33-
self.func = func
34-
35-
def __get__(self, obj, cls):
36-
if obj is None:
37-
return self
38-
value = obj.__dict__[self.func.__name__] = self.func(obj)
39-
return value
40-
4130
def _installed_version(package):
31+
"""Get the installed version of a package.
32+
33+
Parameters
34+
----------
35+
package : str
36+
Name of the package to check.
37+
38+
Returns
39+
-------
40+
str
41+
Version string if available, 'NA' otherwise.
42+
"""
4243
try:
43-
exec(f'import {package}')
44-
except ModuleNotFoundError:
45-
v = 'NA'
46-
try:
47-
v = eval(f'{package}.__version__')
48-
except AttributeError:
49-
v = 'NA'
50-
return v
44+
mod = importlib.import_module(package)
45+
return getattr(mod, "__version__", "NA")
46+
except (ModuleNotFoundError, ImportError):
47+
return "NA"
48+
5149

5250
def _installed_versions():
5351
ver = {}
54-
for package in memberships.keys():
52+
for package in memberships:
5553
ver[package] = _installed_version(package)
5654
return ver
5755

56+
5857
def _released_versions():
5958
from .frozen import frozen_packages
59+
6060
return frozen_packages
6161

6262

@@ -97,17 +97,16 @@ def check(self):
9797
installed = "Installed"
9898
released = "Released"
9999
match = "Match"
100-
s = f'{package:>12} | {installed:>15} | {released:>15} | {match:>5}'
100+
s = f"{package:>12} | {installed:>15} | {released:>15} | {match:>5}"
101101
table.append(s)
102-
table.append("-"*len(s))
102+
table.append("-" * len(s))
103103
for package in self.installed:
104104
installed = self.installed[package]
105105
released = self.released[package]
106106
match = installed == released
107-
s = f'{package:>12} | {installed:>15} | {released:>15} | {match:>5}'
107+
s = f"{package:>12} | {installed:>15} | {released:>15} | {match:>5}"
108108
table.append(s)
109109
print("\n".join(table))
110110

111111

112112
versions = Versions()
113-

pysal/lib/common.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
11
import copy
2-
import sys
3-
import time
4-
5-
# external imports
6-
import numpy as np
7-
import numpy.linalg as la
8-
9-
import scipy as sp
10-
import scipy.stats as stats
11-
from libpysal.cg.kdtree import KDTree
12-
from scipy.spatial.distance import pdist, cdist
13-
14-
import pandas
2+
import importlib
153

164
try:
175
from patsy import PatsyError
186
except ImportError:
197
PatsyError = Exception
208

21-
RTOL = .00001
9+
RTOL = 0.00001
2210
ATOL = 1e-7
2311
MISSINGVALUE = None
2412

@@ -29,27 +17,34 @@
2917
# import numba.jit OR create mimic decorator and set existence flag
3018
try:
3119
from numba import jit
20+
3221
HAS_JIT = True
3322
except ImportError:
34-
def jit(function=None, **kwargs):
35-
"""Mimic numba.jit() with synthetic wrapper
36-
"""
23+
24+
def jit(function=None, **kwargs): # noqa: ARG001
25+
"""Mimic numba.jit() with synthetic wrapper"""
3726
if function is not None:
27+
3828
def wrapped(*original_args, **original_kw):
3929
"""Case 1 - structure of a standard decorator
4030
i.e., jit(function)(*args, **kwargs)
4131
"""
4232
return function(*original_args, **original_kw)
33+
4334
return wrapped
4435
else:
36+
4537
def partial_inner(func):
4638
"""Case 2 - returns Case 1
4739
i.e., jit()(function)(*args, **kwargs)
4840
"""
4941
return jit(func)
42+
5043
return partial_inner
44+
5145
HAS_JIT = False
5246

47+
5348
def simport(modname):
5449
"""
5550
Safely import a module without raising an error.
@@ -58,7 +53,7 @@ def simport(modname):
5853
-----------
5954
modname : str
6055
module name needed to import
61-
56+
6257
Returns
6358
--------
6459
tuple of (True, Module) or (False, None) depending on whether the import
@@ -70,7 +65,6 @@ def simport(modname):
7065
allow the module to be used without necessarily attaching it permanently in
7166
the global namespace:
7267
73-
7468
for t,mod in simport('pandas'):
7569
if t:
7670
mod.DataFrame()
@@ -89,11 +83,12 @@ def simport(modname):
8983
The first idiom makes it work kind of a like a with statement.
9084
"""
9185
try:
92-
exec('import {}'.format(modname))
93-
return True, eval(modname)
94-
except:
86+
mod = importlib.import_module(modname)
87+
return True, mod
88+
except (ModuleNotFoundError, ImportError):
9589
return False, None
9690

91+
9792
def requires(*args, **kwargs):
9893
"""
9994
Decorator to wrap functions with extra dependencies:
@@ -110,19 +105,21 @@ def requires(*args, **kwargs):
110105
Original function is all arg in args are importable, otherwise returns a
111106
function that passes.
112107
"""
113-
v = kwargs.pop('verbose', True)
108+
v = kwargs.pop("verbose", True)
114109
wanted = copy.deepcopy(args)
110+
115111
def inner(function):
116112
available = [simport(arg)[0] for arg in args]
117113
if all(available):
118114
return function
119115
else:
120-
def passer(*args,**kwargs):
116+
117+
def passer(*args, **kwargs): # noqa: ARG001
121118
if v:
122119
missing = [arg for i, arg in enumerate(wanted) if not available[i]]
123-
print(('missing dependencies: {d}'.format(d=missing)))
124-
print(('not running {}'.format(function.__name__)))
125-
else:
126-
pass
120+
print(f"missing dependencies: {missing}")
121+
print(f"not running {function.__name__}")
122+
127123
return passer
124+
128125
return inner

0 commit comments

Comments
 (0)