cs-py-modules 20250724__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cs/py/modules.py ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/python
2
+ #
3
+
4
+ ''' Convenience functions related to modules, packages and importing.
5
+ '''
6
+
7
+ import importlib
8
+ from importlib.machinery import SourceFileLoader
9
+ from importlib.util import spec_from_loader, module_from_spec
10
+ from inspect import getmodule
11
+ import os.path
12
+ import sys
13
+
14
+ from cs.context import contextif, stackattrs
15
+ from cs.gimmicks import warning
16
+ from cs.lex import is_dotted_identifier
17
+ from cs.pfx import Pfx, pfx
18
+
19
+ __version__ = '20250724'
20
+
21
+ DISTINFO = {
22
+ 'keywords': ["python2", "python3"],
23
+ 'classifiers': [
24
+ "Programming Language :: Python",
25
+ "Programming Language :: Python :: 3",
26
+ ],
27
+ 'install_requires': ['cs.context', 'cs.gimmicks', 'cs.lex', 'cs.pfx'],
28
+ }
29
+
30
+ @pfx
31
+ def import_module_name(module_name, name=None, sys_path=None, lock=None):
32
+ ''' Import `module_name` and return the value of `name` within it.
33
+
34
+ Parameters:
35
+ * `module_name`: the module name to import.
36
+ * `name`: optional name within the module whose value is returned;
37
+ if `name` is `None`, return the module itself.
38
+ * `sys_path`optional list of paths to use as `sys.path` during the import.
39
+ * `lock`: an optional lock to hold during the import (recommended).
40
+
41
+ *Warning*: `sys.path` is modified for the duration of this function,
42
+ which may affect concurrent applications.
43
+ '''
44
+ with contextif(lock):
45
+ if sys_path is None:
46
+ sys_path = sys.path
47
+ with stackattrs(sys, path=sys_path):
48
+ try:
49
+ M = importlib.import_module(module_name)
50
+ except ImportError as e:
51
+ # pylint: disable=raise-missing-from
52
+ raise ImportError(
53
+ f'no module named {module_name!r}: {e.__class__.__name__}:{e}'
54
+ ) from e
55
+ if M is not None:
56
+ if name is None:
57
+ return M
58
+ try:
59
+ return getattr(M, name)
60
+ except AttributeError as e:
61
+ # pylint: disable=raise-missing-from
62
+ raise ImportError(
63
+ f'module {module_name!r}: no entry named {name!r} ({e.__class__.__name__}:{e})'
64
+ ) from e
65
+ return None
66
+
67
+ def import_module_from_file(module_name, source_file, sys_path=None):
68
+ ''' Import a specific file as a module instance,
69
+ return the module instance.
70
+
71
+ Parameters:
72
+ * `module_name`: the name to assign to the module
73
+ * `source_file`: the source file to load
74
+ * `sys_path`: optional list of paths to set as `sys.path`
75
+ for the duration of this import;
76
+ the default is the current value of `sys.path`
77
+
78
+ Note that this is a "bare" import;
79
+ the module instance is not inserted into `sys.modules`.
80
+
81
+ *Warning*: `sys.path` is modified for the duration of this function,
82
+ which may affect concurrent applications.
83
+ '''
84
+ if sys_path is None:
85
+ sys_path = sys.path
86
+ with stackattrs(sys, path=sys_path):
87
+ loader = SourceFileLoader(module_name, source_file)
88
+ spec = spec_from_loader(loader.name, loader)
89
+ M = module_from_spec(spec)
90
+ loader.exec_module(M)
91
+ return M
92
+
93
+ def module_files(M):
94
+ ''' Generator yielding `.py` pathnames involved in a module.
95
+ '''
96
+ initpath = M.__file__
97
+ moddir = os.path.dirname(initpath)
98
+ for dirpath, _, filenames in os.walk(moddir):
99
+ for filename in filenames:
100
+ if filename.endswith('.py'):
101
+ yield os.path.join(dirpath, filename)
102
+
103
+ def module_attributes(M):
104
+ ''' Generator yielding the names and values of attributes from a module
105
+ which were defined in the module.
106
+ '''
107
+ for attr in dir(M):
108
+ value = getattr(M, attr, None)
109
+ valueM = getmodule(value)
110
+ if valueM is not None and valueM is not M:
111
+ continue
112
+ yield attr, value
113
+
114
+ def module_names(M):
115
+ ''' Return a list of the names of attributes from a module which were
116
+ defined in the module.
117
+ '''
118
+ return [attr for attr, value in module_attributes(M)]
119
+
120
+ # TODO: use the AST module to do a real parse?
121
+ # pylint: disable=too-many-branches
122
+ @pfx
123
+ def direct_imports(src_filename, module_name):
124
+ ''' Crudely parse `src_filename` for `import` statements.
125
+ Return the set of directly imported module names.
126
+
127
+ Resolve relative imports against `module_name`.
128
+
129
+ This is a very simple minded source parse.
130
+ '''
131
+ subnames = set()
132
+ with open(src_filename, encoding='utf-8') as codefp:
133
+ for lineno, line in enumerate(codefp, 1):
134
+ line = line.strip()
135
+ with Pfx("%d: %s", lineno, line):
136
+ if line.startswith('import ') or line.startswith('from '):
137
+ # quick hack to strip trailing "; second-statement"
138
+ try:
139
+ line, _ = line.split(';', 1)
140
+ except ValueError:
141
+ pass
142
+ words = line.split()
143
+ if not words:
144
+ continue
145
+ word0 = words[0]
146
+ if word0 not in ('from', 'import'):
147
+ continue
148
+ if len(words) < 2:
149
+ continue
150
+ if word0 == 'from' and (len(words) < 4 or words[2] != 'import'):
151
+ continue
152
+ subimport = words[1]
153
+ if subimport.startswith('.'):
154
+ # relative import, resolve against module_name
155
+ subimport0 = subimport
156
+ module_parts = module_name.split('.')
157
+ while subimport.startswith('.'):
158
+ module_parts = module_parts[:-1]
159
+ subimport = subimport[1:]
160
+ if not module_parts:
161
+ # walked off the top of the path
162
+ warning(
163
+ "import %r walked out of the module %r", subimport0,
164
+ module_name
165
+ )
166
+ continue
167
+ module_parts.append(subimport or '__init__')
168
+ subimport = '.'.join(module_parts)
169
+ if subimport == module_name:
170
+ # HACK: simplistic parse finding ourself in a docstring
171
+ continue
172
+ if not is_dotted_identifier(subimport):
173
+ warning("ignoring %r, not a dotted identifier", subimport)
174
+ continue
175
+ subnames.add(subimport)
176
+ return subnames
177
+
178
+ def import_extra(extra_package_name, distinfo):
179
+ ''' Try to import the package named `extra_package_name`
180
+ using `importlib.import_module`. Return the imported package.
181
+
182
+ If an `ImportError` is raised,
183
+ riffle through the extras mapping in `distinfo['extras_requires']`
184
+ for the package name, and emit an informative warning
185
+ about the extras which require this package
186
+ and whose use a `pip install` time would bring the package in.
187
+ The original `ImportError` is then reraised.
188
+
189
+ If no extra is found this is presumed to be an error by the caller
190
+ and a `RuntimeError` is raised.
191
+ This function is for internal use as:
192
+
193
+ pkg = import_extra('some_package', DISTINFO)
194
+
195
+ which passes in the source module's `DISTINFO` mapping,
196
+ which I use as the basis for my package distributions.
197
+
198
+ A fuller example from my `cs.timeseries` module's
199
+ `plot` command line mode:
200
+
201
+ def cmd_plot(self, argv):
202
+ """ Usage: {cmd} datadir days fields...
203
+ """
204
+ try:
205
+ import_extra('plotly', DISTINFO)
206
+ except ImportError as e:
207
+ raise GetoptError(
208
+ "the plotly package is not installed: %s" % (e,)
209
+ ) from e
210
+
211
+ which produces this output:
212
+
213
+ timeseries.py: plot: import_extra('plotly'): package not available; the following extras pull it in: ['plotting']
214
+ timeseries.py: the plotly package is not installed: timeseries.py: plot: import_extra('plotly'): No module named 'plotly'
215
+ '''
216
+ with Pfx("import_extra(%r)", extra_package_name):
217
+ try:
218
+ return importlib.import_module(extra_package_name)
219
+ except ImportError:
220
+ from_extras = [
221
+ extra_name
222
+ for extra_name, extra_packages in distinfo['extras_requires'].items()
223
+ if extra_package_name in extra_packages
224
+ ]
225
+ if from_extras:
226
+ warning(
227
+ "package not available; the following extras pull it in: %r",
228
+ sorted(from_extras)
229
+ )
230
+ raise
231
+ # pylint: disable=raise-missing-from
232
+ raise RuntimeError(
233
+ "import_extra called with a package not listed in DISTINFO[extras_requires]=%r"
234
+ % (DISTINFO['extras_requires'],)
235
+ )
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-py-modules
3
+ Version: 20250724
4
+ Summary: Convenience functions related to modules, packages and importing.
5
+ Keywords: python2,python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.context>=20250528
16
+ Requires-Dist: cs.gimmicks>=20250428
17
+ Requires-Dist: cs.lex>=20250428
18
+ Requires-Dist: cs.pfx>=20250613
19
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
20
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
21
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
22
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/py/modules.py
23
+
24
+ Convenience functions related to modules, packages and importing.
25
+
26
+ *Latest release 20250724*:
27
+ * Improve resolution of relative imports. (Fix seems to be an overly strong word here, unfortunately.)
28
+ * Drop pretence at Python 2 support.
29
+
30
+ Short summary:
31
+ * `direct_imports`: Crudely parse `src_filename` for `import` statements. Return the set of directly imported module names.
32
+ * `import_extra`: Try to import the package named `extra_package_name` using `importlib.import_module`.
33
+ * `import_module_from_file`: Import a specific file as a module instance, return the module instance.
34
+ * `import_module_name`: Import `module_name` and return the value of `name` within it.
35
+ * `module_attributes`: Generator yielding the names and values of attributes from a module which were defined in the module.
36
+ * `module_files`: Generator yielding `.py` pathnames involved in a module.
37
+ * `module_names`: Return a list of the names of attributes from a module which were defined in the module.
38
+
39
+ Module contents:
40
+ - <a name="direct_imports"></a>`direct_imports(src_filename, module_name)`: Crudely parse `src_filename` for `import` statements.
41
+ Return the set of directly imported module names.
42
+
43
+ Resolve relative imports against `module_name`.
44
+
45
+ This is a very simple minded source parse.
46
+ - <a name="import_extra"></a>`import_extra(extra_package_name, distinfo)`: Try to import the package named `extra_package_name`
47
+ using `importlib.import_module`. Return the imported package.
48
+
49
+ If an `ImportError` is raised,
50
+ riffle through the extras mapping in `distinfo['extras_requires']`
51
+ for the package name, and emit an informative warning
52
+ about the extras which require this package
53
+ and whose use a `pip install` time would bring the package in.
54
+ The original `ImportError` is then reraised.
55
+
56
+ If no extra is found this is presumed to be an error by the caller
57
+ and a `RuntimeError` is raised.
58
+ This function is for internal use as:
59
+
60
+ pkg = import_extra('some_package', DISTINFO)
61
+
62
+ which passes in the source module's `DISTINFO` mapping,
63
+ which I use as the basis for my package distributions.
64
+
65
+ A fuller example from my `cs.timeseries` module's
66
+ `plot` command line mode:
67
+
68
+ def cmd_plot(self, argv):
69
+ """ Usage: {cmd} datadir days fields...
70
+ """
71
+ try:
72
+ import_extra('plotly', DISTINFO)
73
+ except ImportError as e:
74
+ raise GetoptError(
75
+ "the plotly package is not installed: %s" % (e,)
76
+ ) from e
77
+
78
+ which produces this output:
79
+
80
+ timeseries.py: plot: import_extra('plotly'): package not available; the following extras pull it in: ['plotting']
81
+ timeseries.py: the plotly package is not installed: timeseries.py: plot: import_extra('plotly'): No module named 'plotly'
82
+ - <a name="import_module_from_file"></a>`import_module_from_file(module_name, source_file, sys_path=None)`: Import a specific file as a module instance,
83
+ return the module instance.
84
+
85
+ Parameters:
86
+ * `module_name`: the name to assign to the module
87
+ * `source_file`: the source file to load
88
+ * `sys_path`: optional list of paths to set as `sys.path`
89
+ for the duration of this import;
90
+ the default is the current value of `sys.path`
91
+
92
+ Note that this is a "bare" import;
93
+ the module instance is not inserted into `sys.modules`.
94
+
95
+ *Warning*: `sys.path` is modified for the duration of this function,
96
+ which may affect concurrent applications.
97
+ - <a name="import_module_name"></a>`import_module_name(module_name, name=None, sys_path=None, lock=None)`: Import `module_name` and return the value of `name` within it.
98
+
99
+ Parameters:
100
+ * `module_name`: the module name to import.
101
+ * `name`: optional name within the module whose value is returned;
102
+ if `name` is `None`, return the module itself.
103
+ * `sys_path`optional list of paths to use as `sys.path` during the import.
104
+ * `lock`: an optional lock to hold during the import (recommended).
105
+
106
+ *Warning*: `sys.path` is modified for the duration of this function,
107
+ which may affect concurrent applications.
108
+ - <a name="module_attributes"></a>`module_attributes(M)`: Generator yielding the names and values of attributes from a module
109
+ which were defined in the module.
110
+ - <a name="module_files"></a>`module_files(M)`: Generator yielding `.py` pathnames involved in a module.
111
+ - <a name="module_names"></a>`module_names(M)`: Return a list of the names of attributes from a module which were
112
+ defined in the module.
113
+
114
+ # Release Log
115
+
116
+
117
+
118
+ *Release 20250724*:
119
+ * Improve resolution of relative imports. (Fix seems to be an overly strong word here, unfortunately.)
120
+ * Drop pretence at Python 2 support.
121
+
122
+ *Release 20241122*:
123
+ import_module_name: rename the `path` parameter to `sys_path` to match import_module_from_file, default sys_path to sys.path.
124
+
125
+ *Release 20240630*:
126
+ direct_imports: fix off-by-one resolving leading dot relative import names.
127
+
128
+ *Release 20220606*:
129
+ New import_extra(extra_package_name,distinfo) function to politely try to import a package which is associated with an extra.
130
+
131
+ *Release 20210123*:
132
+ module_attributes: skip values from other modules _if we know the module_ (computed values like tuples have no module and still need to be returned).
133
+
134
+ *Release 20200521*:
135
+ * New import_module_from_file function to import a Python file as a module instance.
136
+ * New direct_imports(src_filename,module_name=None) returning the set of directly imports module names.
137
+
138
+ *Release 20190101*:
139
+ New functions: module_names, module_attributes.
140
+
141
+ *Release 20160918*:
142
+ * New generator function module_files yielding pathnames.
143
+ * import_module_name: accept name=None, just return the module.
144
+ * Add empty "install_requires" for DISTINFO completeness.
145
+
146
+ *Release 20150116*:
147
+ Initial PyPI release.
@@ -0,0 +1,4 @@
1
+ cs/py/modules.py,sha256=0XtG3UCYHT4tAuxg-rC_tJJO4LcxnMZFNUf-XHGHQz4,8167
2
+ cs_py_modules-20250724.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
3
+ cs_py_modules-20250724.dist-info/METADATA,sha256=g0NeVf8ihpzOzyaOmX-cqiEfheq8rfPSyyNYYqChhJI,6797
4
+ cs_py_modules-20250724.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any