From 2c433e210273db548c19f54fde1c9b8b32a6dcce Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 2 Nov 2020 12:51:48 +1100 Subject: [PATCH 1/3] Added API docs for versions submodule added in #65 Formatted docstrings to match sphinx Doogle style and added rst file to docs to display them. --- docs/source/api/index.rst | 1 + docs/source/api/versions.rst | 8 +++ labscript_utils/versions.py | 111 ++++++++++++++++++++++++++++------- 3 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 docs/source/api/versions.rst diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 024f52a..9e9ff9a 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -60,3 +60,4 @@ Module and File Tools double_import_denier filewatcher modulewatcher + versions diff --git a/docs/source/api/versions.rst b/docs/source/api/versions.rst new file mode 100644 index 0000000..996d686 --- /dev/null +++ b/docs/source/api/versions.rst @@ -0,0 +1,8 @@ +============================ +labscript_utils.versions +============================ + +.. automodule:: labscript_utils.versions + :members: + :undoc-members: + :private-members: \ No newline at end of file diff --git a/labscript_utils/versions.py b/labscript_utils/versions.py index 0b84ed5..032bbf7 100644 --- a/labscript_utils/versions.py +++ b/labscript_utils/versions.py @@ -48,7 +48,20 @@ class BrokenInstall(RuntimeError): def get_import_path(import_name): """Get which entry in sys.path a module would be imported from, without importing - it.""" + it. + + Args: + import_name (str): The module name. + + Raises: + ModuleNotFoundError: Raised if the module is not installed. + NotImplementedError: Raised if the module is a "namespace package". + Support for namepsace packages is not currently availabled. + + Returns: + str: The path to the folder containing the module. + + """ spec = importlib.util.find_spec(import_name) if spec is None: raise ModuleNotFoundError(import_name) @@ -66,8 +79,22 @@ def get_import_path(import_name): def _get_metadata_version(project_name, import_path): - """Return the metadata version for a package with the given project name located at - the given import path, or None if there is no such package.""" + """Gets the package metadata version. + + Args: + project_name (str): The package name (e.g. the name used when pip installing + the package). + import_path (str): The path to the folder containing the installed package. + + Raises: + :exc:`BrokenInstall`: Raised if the package installation is corrupted (multiple + packages matching the given arguments were found). May occur if + (un)installation for a particular package version only partially completed. + + Returns: + The metadata version for a package with the given project name located at + the given import path, or None if there is no such package. + """ for finder in sys.meta_path: if hasattr(finder, 'find_distributions'): @@ -84,8 +111,14 @@ def _get_metadata_version(project_name, import_path): def _get_literal_version(filename): - """Tokenize a source file and return any __version__ = literal defined in - it. + """Tokenize a source file and return any :code:`__version__ = ` literal + defined in it. + + Args: + filename (str): The path to the file to tokenize. + + Returns: + Any version literal found matching the above criteria, or None. """ if not os.path.exists(filename): return None @@ -110,17 +143,34 @@ def _get_literal_version(filename): def get_version(import_name, project_name=None, import_path=None): - """Try very hard to get the version of a package without importing it. if - import_path is not given, first find where it would be imported from, without + """Try very hard to get the version of a package without importing it. + + If import_path is not given, first find where it would be imported from, without importing it. Then look for metadata in the same import path with the given project name (note: this is not always the same as the import name, it is the name for example you would ask pip to install). If that is found, return the version info - from it. Otherwise look for a __version__.py file in the package directory, or a - __version__ = literal defined in the package source (without executing - it). + from it. Otherwise look for a :code:`__version__.py` file in the package directory, + or a :code:`__version__ = ` literal defined in the package source (without + executing it). + + Args: + import_name (str): The module name. + project_name (str, optional): The package name (e.g. the name used when pip + installing the package). This must be specified if it does not match the + module name. + import_path (str, optional): The path to the folder containing the installed + package. + + Raises: + NotImplementedError: Raised if the module name contains a period. Only + top-level packages are supported at this time. - Return NotFound if the package cannot be found, and NoVersionInfo if the version - cannot be obtained in the above way, or if it was found but was None.""" + Returns: + The version literal of the package. + If the package cannot be found, :class:`NotFound` is returned. + If the version cannot be obtained in the above way, or if the version was found + but was :code:`None`, :class:`NoVersionInfo` is returned. + """ if project_name is None: project_name = import_name if '.' in import_name: @@ -162,15 +212,34 @@ def get_version(import_name, project_name=None, import_path=None): def check_version(module_name, at_least, less_than, version=None, project_name=None): - """Check that the version of the given module is at least and less than the given - version strings, and raise VersionException if not. Raise VersionException if the - module was not found or its version could not be determined. This function uses - get_version to determine version numbers without importing modules. In order to do - this, project_name must be provided if it differs from module_name. For example, - pyserial is imported as 'serial', but the project name, as passed to a 'pip install' - command, is 'pyserial'. Therefore to check the version of pyserial, pass in - module_name='serial' and project_name='pyserial'. You can also pass in a version - string yourself, in which case no inspection of packages will take place. + """Checks if a module version is within specified bounds. + + Checks that the version of the given module is at least and less than the given + version strings. This function uses :func:`get_version` to determine version + numbers without importing modules. In order to do this, :code:`project_name` must + be provided if it differs from :code:`module_name`. For example, pyserial is + imported as 'serial', but the project name, as passed to a 'pip install' command, + is 'pyserial'. Therefore to check the version of pyserial, pass in + :code:`module_name='serial'` and :code:`project_name='pyserial'`. + You can also pass in a version string yourself, in which case no inspection of + packages will take place. + + Args: + module_name (str): The name of the module to check. + at_least (str): The minimum acceptable module version. + less_than (str): The minimum unacceptable module version. Usually this would be + the next major version if the package follows + `semver `_. + version (str, optional): The current version of the installed package. Useful when the + package version is stored in a non-standard location. + project_name (str, optional): The package name (e.g. the name used when pip + installing the package). This must be specified if it does not match the + module name. + + Raises: + :exc:`VersionException`: if the module was not found or its version could not + be determined. + """ if version is None: version = get_version(module_name, project_name) From 83c6ded5e7c6911aaf096ef3121966e013ca9a39 Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 2 Nov 2020 13:04:42 +1100 Subject: [PATCH 2/3] Attempt to fix the site module issue on readthedocs. --- docs/source/conf.py | 1 + labscript_utils/modulewatcher.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 675d806..10120f4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -49,6 +49,7 @@ ] autodoc_typehints = 'description' +autodoc_mock_imports = ["site"] # Prefix each autosectionlabel with the name of the document it is in and a colon autosectionlabel_prefix_document = True diff --git a/labscript_utils/modulewatcher.py b/labscript_utils/modulewatcher.py index 6b0be0e..7e33b6d 100644 --- a/labscript_utils/modulewatcher.py +++ b/labscript_utils/modulewatcher.py @@ -33,6 +33,14 @@ class ModuleWatcher(object): + """A watcher that reloads modules that have been modified on disk + + Only reloads modules imported after instantiation. Does not reload C extensions. + + Args: + debug (bool, optional): When :code:`True`, prints debugging information + when reloading modules. + """ def __init__(self, debug=False): self.debug = debug # A lock to hold whenever you don't want modules unloaded: From eaa13a025df534ddc64e1bc963f6db07c1f52694 Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 2 Nov 2020 16:24:02 +1100 Subject: [PATCH 3/3] Second attempt at fixing the missing site package functions. This time we explicitly mock the missing functions using a lambda that returns an empty version of whatever types the methods usually return (hard coded into conf.py) --- docs/source/conf.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 10120f4..e3df35c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,7 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # +import copy import os from pathlib import Path from m2r import MdInclude @@ -49,7 +50,23 @@ ] autodoc_typehints = 'description' -autodoc_mock_imports = ["site"] + +# mock missing site packages methods +import site +mock_site_methods = { + # Format: + # method name: return value + 'getusersitepackages': '', + 'getsitepackages': [] +} +__fn = None +for __name, __rval in mock_site_methods.items(): + if not hasattr(site, __name): + __fn = lambda *args, __rval=copy.deepcopy(__rval), **kwargs: __rval + setattr(site, __name, __fn) +del __name +del __rval +del __fn # Prefix each autosectionlabel with the name of the document it is in and a colon autosectionlabel_prefix_document = True