Skip to content

Commit 5f8280f

Browse files
Merge pull request #65 from zakv/restore-check_version
Restore check_version()
2 parents af57473 + 91b7562 commit 5f8280f

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

labscript_utils/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def import_or_reload(modulename):
4747
return module
4848

4949

50+
from labscript_utils.versions import VersionException, check_version
51+
52+
5053
def dedent(s):
5154
"""Remove leading spaces from the first line of a string, all common leading
5255
indentation (spaces only) from subsequent lines, strip trailing spaces from all

labscript_utils/versions.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#####################################################################
2+
# #
3+
# versions.py #
4+
# #
5+
# Copyright 2019, Chris Billington #
6+
# #
7+
# This file is part of the labscript suite (see #
8+
# http://labscriptsuite.org) and is licensed under the Simplified #
9+
# BSD License. See the license.txt file in the root of the project #
10+
# for the full license. #
11+
# #
12+
#####################################################################
13+
import sys
14+
import os
15+
import importlib
16+
import tokenize
17+
import ast
18+
import setuptools_scm
19+
import packaging.version
20+
21+
try:
22+
import importlib.metadata as importlib_metadata
23+
except ImportError:
24+
import importlib_metadata
25+
26+
27+
class NotFound(object):
28+
pass
29+
30+
31+
class NoVersionInfo(object):
32+
pass
33+
34+
35+
class VersionException(RuntimeError):
36+
pass
37+
38+
39+
class BrokenInstall(RuntimeError):
40+
pass
41+
42+
43+
ERR_BROKEN_INSTALL = """Multiple metadata files for {package} found in {path}; cannot
44+
reliably get version information. This indicates a previous version of the package was
45+
not properly removed. You may want to uninstall the package, manually delete remaining
46+
metadata files/folders, then reinstall the package.""".replace('\n', ' ')
47+
48+
49+
def get_import_path(import_name):
50+
"""Get which entry in sys.path a module would be imported from, without importing
51+
it."""
52+
spec = importlib.util.find_spec(import_name)
53+
if spec is None:
54+
raise ModuleNotFoundError(import_name)
55+
location = spec.origin
56+
if location is None:
57+
# A namespace package
58+
msg = "Version checking of namespace packages not implemented"
59+
raise NotImplementedError(msg)
60+
if spec.parent:
61+
# A package:
62+
return os.path.dirname(os.path.dirname(location))
63+
else:
64+
# A single-file module:
65+
return os.path.dirname(location)
66+
67+
68+
def _get_metadata_version(project_name, import_path):
69+
"""Return the metadata version for a package with the given project name located at
70+
the given import path, or None if there is no such package."""
71+
72+
for finder in sys.meta_path:
73+
if hasattr(finder, 'find_distributions'):
74+
context = importlib_metadata.DistributionFinder.Context(
75+
name=project_name, path=[import_path]
76+
)
77+
dists = finder.find_distributions(context)
78+
dists = list(dists)
79+
if len(dists) > 1:
80+
msg = ERR_BROKEN_INSTALL.format(package=project_name, path=import_path)
81+
raise BrokenInstall(msg)
82+
if dists:
83+
return dists[0].version
84+
85+
86+
def _get_literal_version(filename):
87+
"""Tokenize a source file and return any __version__ = <version> literal defined in
88+
it.
89+
"""
90+
if not os.path.exists(filename):
91+
return None
92+
with open(filename, 'r') as f:
93+
try:
94+
tokens = list(tokenize.generate_tokens(f.readline))
95+
except tokenize.TokenError:
96+
tokens = []
97+
for i, token in enumerate(tokens[:-2]):
98+
token_type, token_str, _, _, _ = token
99+
if token_type == tokenize.NAME and token_str == '__version__':
100+
next_token_type, next_token_str, _, _, _ = tokens[i + 1]
101+
if next_token_type == tokenize.OP and next_token_str == '=':
102+
next_next_token_type, next_next_token_str, _, _, _ = tokens[i + 2]
103+
if next_next_token_type == tokenize.STRING:
104+
try:
105+
version = ast.literal_eval(next_next_token_str)
106+
if version is not None:
107+
return version
108+
except (SyntaxError, ValueError):
109+
continue
110+
111+
112+
def get_version(import_name, project_name=None, import_path=None):
113+
"""Try very hard to get the version of a package without importing it. if
114+
import_path is not given, first find where it would be imported from, without
115+
importing it. Then look for metadata in the same import path with the given project
116+
name (note: this is not always the same as the import name, it is the name for
117+
example you would ask pip to install). If that is found, return the version info
118+
from it. Otherwise look for a __version__.py file in the package directory, or a
119+
__version__ = <version> literal defined in the package source (without executing
120+
it).
121+
122+
Return NotFound if the package cannot be found, and NoVersionInfo if the version
123+
cannot be obtained in the above way, or if it was found but was None."""
124+
if project_name is None:
125+
project_name = import_name
126+
if '.' in import_name:
127+
msg = "Version checking of top-level packages only implemented"
128+
raise NotImplementedError(msg)
129+
if import_path is None:
130+
# Find the path where the module lives:
131+
try:
132+
import_path = get_import_path(import_name)
133+
except ImportError:
134+
return NotFound
135+
if not os.path.exists(os.path.join(import_path, import_name)):
136+
return NotFound
137+
try:
138+
# Check if setuptools_scm gives us a version number, for the case that it's a
139+
# git repo or PyPI tarball:
140+
return setuptools_scm.get_version(import_path)
141+
except LookupError:
142+
pass
143+
# Check if importlib_metadata knows about this module:
144+
version = _get_metadata_version(project_name, import_path)
145+
if version is not None:
146+
return version
147+
# Check if it has a version literal defined in a __version__.py file:
148+
version_dot_py = os.path.join(import_path, import_name, '__version__.py')
149+
version = _get_literal_version(version_dot_py)
150+
if version is not None:
151+
return version
152+
# check if it has a __version__ literal defined in its main module.
153+
pkg = os.path.join(import_path, import_name)
154+
if os.path.isdir(pkg):
155+
module_file = os.path.join(pkg, '__init__.py')
156+
else:
157+
module_file = pkg + '.py'
158+
version = _get_literal_version(module_file)
159+
if version is not None:
160+
return version
161+
return NoVersionInfo
162+
163+
164+
def check_version(module_name, at_least, less_than, version=None, project_name=None):
165+
"""Check that the version of the given module is at least and less than the given
166+
version strings, and raise VersionException if not. Raise VersionException if the
167+
module was not found or its version could not be determined. This function uses
168+
get_version to determine version numbers without importing modules. In order to do
169+
this, project_name must be provided if it differs from module_name. For example,
170+
pyserial is imported as 'serial', but the project name, as passed to a 'pip install'
171+
command, is 'pyserial'. Therefore to check the version of pyserial, pass in
172+
module_name='serial' and project_name='pyserial'. You can also pass in a version
173+
string yourself, in which case no inspection of packages will take place.
174+
"""
175+
if version is None:
176+
version = get_version(module_name, project_name)
177+
178+
if version is NotFound:
179+
raise VersionException('Module {} not found'.format(module_name))
180+
181+
if version is NoVersionInfo:
182+
raise VersionException(
183+
'Could not get version info from module {}'.format(module_name)
184+
)
185+
186+
at_least_version, less_than_version, installed_version = [
187+
packaging.version.parse(v) for v in [at_least, less_than, version]
188+
]
189+
190+
if not at_least_version <= installed_version < less_than_version:
191+
msg = (
192+
'{module_name} {version} found. '
193+
+ '{at_least} <= {module_name} < {less_than} required.'
194+
)
195+
raise VersionException(msg.format(**locals()))
196+
197+
198+
if __name__ == '__main__':
199+
assert get_version('subprocess') == NoVersionInfo
200+
assert get_version('plsgtbg') == NotFound
201+
assert type(get_version('labscript_utils')) in [str, bytes]
202+
assert type(get_version('numpy')) in [str, bytes]
203+
assert type(get_version('serial', 'pyserial')) in [str, bytes]

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy