immlib 0.1.0__tar.gz

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.
Files changed (50) hide show
  1. immlib-0.1.0/LICENSE +21 -0
  2. immlib-0.1.0/PKG-INFO +76 -0
  3. immlib-0.1.0/README.md +26 -0
  4. immlib-0.1.0/pyproject.toml +48 -0
  5. immlib-0.1.0/setup.cfg +4 -0
  6. immlib-0.1.0/src/immlib/__init__.py +131 -0
  7. immlib-0.1.0/src/immlib/_init.py +108 -0
  8. immlib-0.1.0/src/immlib/_version.py +239 -0
  9. immlib-0.1.0/src/immlib/doc/__init__.py +38 -0
  10. immlib-0.1.0/src/immlib/doc/_core.py +312 -0
  11. immlib-0.1.0/src/immlib/iolib/__init__.py +29 -0
  12. immlib-0.1.0/src/immlib/iolib/_core.py +720 -0
  13. immlib-0.1.0/src/immlib/pathlib/__init__.py +69 -0
  14. immlib-0.1.0/src/immlib/pathlib/_cache.py +152 -0
  15. immlib-0.1.0/src/immlib/pathlib/_core.py +883 -0
  16. immlib-0.1.0/src/immlib/pathlib/_osf.py +538 -0
  17. immlib-0.1.0/src/immlib/test/__init__.py +16 -0
  18. immlib-0.1.0/src/immlib/test/__main__.py +10 -0
  19. immlib-0.1.0/src/immlib/test/doc/__init__.py +6 -0
  20. immlib-0.1.0/src/immlib/test/doc/test_core.py +91 -0
  21. immlib-0.1.0/src/immlib/test/iolib/__init__.py +7 -0
  22. immlib-0.1.0/src/immlib/test/iolib/test_core.py +81 -0
  23. immlib-0.1.0/src/immlib/test/pathlib/__init__.py +11 -0
  24. immlib-0.1.0/src/immlib/test/pathlib/test_core.py +146 -0
  25. immlib-0.1.0/src/immlib/test/pathlib/test_osf.py +54 -0
  26. immlib-0.1.0/src/immlib/test/types/__init__.py +5 -0
  27. immlib-0.1.0/src/immlib/test/types/test_core.py +110 -0
  28. immlib-0.1.0/src/immlib/test/util/__init__.py +11 -0
  29. immlib-0.1.0/src/immlib/test/util/test_core.py +681 -0
  30. immlib-0.1.0/src/immlib/test/util/test_numeric.py +1374 -0
  31. immlib-0.1.0/src/immlib/test/util/test_quantity.py +218 -0
  32. immlib-0.1.0/src/immlib/test/util/test_url.py +51 -0
  33. immlib-0.1.0/src/immlib/test/workflow/__init__.py +9 -0
  34. immlib-0.1.0/src/immlib/test/workflow/test_core.py +418 -0
  35. immlib-0.1.0/src/immlib/test/workflow/test_plantype.py +248 -0
  36. immlib-0.1.0/src/immlib/types/__init__.py +29 -0
  37. immlib-0.1.0/src/immlib/types/_core.py +333 -0
  38. immlib-0.1.0/src/immlib/util/__init__.py +283 -0
  39. immlib-0.1.0/src/immlib/util/_core.py +2524 -0
  40. immlib-0.1.0/src/immlib/util/_numeric.py +2651 -0
  41. immlib-0.1.0/src/immlib/util/_quantity.py +523 -0
  42. immlib-0.1.0/src/immlib/util/_url.py +114 -0
  43. immlib-0.1.0/src/immlib/workflow/__init__.py +48 -0
  44. immlib-0.1.0/src/immlib/workflow/_core.py +1636 -0
  45. immlib-0.1.0/src/immlib/workflow/_plantype.py +337 -0
  46. immlib-0.1.0/src/immlib.egg-info/PKG-INFO +76 -0
  47. immlib-0.1.0/src/immlib.egg-info/SOURCES.txt +48 -0
  48. immlib-0.1.0/src/immlib.egg-info/dependency_links.txt +1 -0
  49. immlib-0.1.0/src/immlib.egg-info/requires.txt +19 -0
  50. immlib-0.1.0/src/immlib.egg-info/top_level.txt +1 -0
immlib-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Noah C. Benson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
immlib-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: immlib
3
+ Version: 0.1.0
4
+ Summary: A library of utilites for immutable scientific data.
5
+ Author-email: "Noah C. Benson" <nben@uw.edu>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/noahbenson/immlib
8
+ Project-URL: documentation, https://github.com/noahbenson/immlib
9
+ Project-URL: repository, https://github.com/noahbenson/immlib
10
+ Keywords: persistent,immutable,functional,scientific,workflow
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
26
+ Classifier: Operating System :: Microsoft :: Windows
27
+ Classifier: Operating System :: POSIX
28
+ Classifier: Operating System :: Unix
29
+ Classifier: Operating System :: MacOS
30
+ Requires-Python: >=3.8
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: pcollections>=0.3.2
34
+ Requires-Dist: numpy>=1.24.0
35
+ Requires-Dist: scipy>=1.7.0
36
+ Requires-Dist: pint>=0.20.0
37
+ Requires-Dist: docrep>=0.3.2
38
+ Requires-Dist: joblib>=1.3.0
39
+ Requires-Dist: cloudpathlib[azure,gs,s3]>=0.18.0
40
+ Requires-Dist: pyyaml>=6.0
41
+ Provides-Extra: dev
42
+ Requires-Dist: torch>=2.2.0; extra == "dev"
43
+ Provides-Extra: test
44
+ Requires-Dist: torch>=2.2.0; extra == "test"
45
+ Provides-Extra: docs
46
+ Requires-Dist: torch>=2.2.0; extra == "docs"
47
+ Requires-Dist: jupyter-book>=1.0.0; extra == "docs"
48
+ Requires-Dist: matplotlib>=3.4.0; extra == "docs"
49
+ Dynamic: license-file
50
+
51
+ ![immlib](https://noahbenson.github.io/immlib/_static/logo.svg "immlib")
52
+
53
+ ![Build Status](https://github.com/noahbenson/immlib/actions/workflows/tests.yml/badge.svg)
54
+ [![codecov](https://codecov.io/gh/noahbenson/immlib/graph/badge.svg?token=8KO3K6DUX4)](https://codecov.io/gh/noahbenson/immlib)
55
+ [![PyPI version](https://badge.fury.io/py/immlib.svg)](https://badge.fury.io/py/immlib)
56
+
57
+ ---
58
+
59
+ **Author**: Noah C. Benson &lt;[nben@uw.edu](mailto:nben@uw.edu)&gt;
60
+ **License**: MIT
61
+ **[Documentation](https://noahbenson.github.io/immlib)**
62
+
63
+ ---
64
+
65
+ `immlib` is a lightweight Python library that simplifies the design of
66
+ application programming interfaces (APIs) for scientific libraries. The name
67
+ immlib comes from the library’s philosophy of using immutable data to simplify
68
+ scientific workflows.
69
+
70
+ `immlib` is heavily based on the library
71
+ [`pimms`](https://github.com/noahbenson/pimms), which effectively served as a
72
+ prototype for `immlib`. Both libraries were motivated by a number of observations
73
+ about the design of scientific software and are an attempt to make some of these
74
+ problems easier to manage.
75
+
76
+ For more information, see the [documentation](https://noahbenson.github.io/immlib).
immlib-0.1.0/README.md ADDED
@@ -0,0 +1,26 @@
1
+ ![immlib](https://noahbenson.github.io/immlib/_static/logo.svg "immlib")
2
+
3
+ ![Build Status](https://github.com/noahbenson/immlib/actions/workflows/tests.yml/badge.svg)
4
+ [![codecov](https://codecov.io/gh/noahbenson/immlib/graph/badge.svg?token=8KO3K6DUX4)](https://codecov.io/gh/noahbenson/immlib)
5
+ [![PyPI version](https://badge.fury.io/py/immlib.svg)](https://badge.fury.io/py/immlib)
6
+
7
+ ---
8
+
9
+ **Author**: Noah C. Benson &lt;[nben@uw.edu](mailto:nben@uw.edu)&gt;
10
+ **License**: MIT
11
+ **[Documentation](https://noahbenson.github.io/immlib)**
12
+
13
+ ---
14
+
15
+ `immlib` is a lightweight Python library that simplifies the design of
16
+ application programming interfaces (APIs) for scientific libraries. The name
17
+ immlib comes from the library’s philosophy of using immutable data to simplify
18
+ scientific workflows.
19
+
20
+ `immlib` is heavily based on the library
21
+ [`pimms`](https://github.com/noahbenson/pimms), which effectively served as a
22
+ prototype for `immlib`. Both libraries were motivated by a number of observations
23
+ about the design of scientific software and are an attempt to make some of these
24
+ problems easier to manage.
25
+
26
+ For more information, see the [documentation](https://noahbenson.github.io/immlib).
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "immlib"
3
+ version = "0.1.0"
4
+ description = """A library of utilites for immutable scientific data."""
5
+ authors = [{name="Noah C. Benson", email="nben@uw.edu"}]
6
+ license = {"text"="MIT"}
7
+ readme = "README.md"
8
+ requires-python = ">=3.8"
9
+ keywords = ["persistent", "immutable", "functional", "scientific", "workflow"]
10
+ classifiers = [
11
+ 'Intended Audience :: Science/Research',
12
+ 'Intended Audience :: Developers',
13
+ 'License :: OSI Approved :: MIT License',
14
+ 'Programming Language :: Python :: 3',
15
+ 'Programming Language :: Python :: 3.8',
16
+ 'Programming Language :: Python :: 3.9',
17
+ 'Programming Language :: Python :: 3.10',
18
+ 'Programming Language :: Python :: 3.11',
19
+ 'Programming Language :: Python :: 3.12',
20
+ 'Programming Language :: Python :: 3.13',
21
+ 'Topic :: Software Development',
22
+ 'Topic :: Software Development :: Libraries',
23
+ 'Topic :: Software Development :: Libraries :: Python Modules',
24
+ 'Topic :: Scientific/Engineering',
25
+ 'Topic :: Scientific/Engineering :: Information Analysis',
26
+ 'Operating System :: Microsoft :: Windows',
27
+ 'Operating System :: POSIX',
28
+ 'Operating System :: Unix',
29
+ 'Operating System :: MacOS']
30
+ dependencies = [
31
+ 'pcollections >= 0.3.2',
32
+ 'numpy >= 1.24.0',
33
+ 'scipy >= 1.7.0',
34
+ 'pint >= 0.20.0',
35
+ 'docrep >= 0.3.2',
36
+ 'joblib >= 1.3.0',
37
+ 'cloudpathlib[s3,gs,azure] >= 0.18.0',
38
+ 'pyyaml >= 6.0']
39
+
40
+ [project.optional-dependencies]
41
+ dev = ["torch >= 2.2.0"]
42
+ test = ["torch >= 2.2.0"]
43
+ docs = ["torch >= 2.2.0", "jupyter-book >= 1.0.0", "matplotlib >= 3.4.0"]
44
+
45
+ [project.urls]
46
+ homepage = "https://github.com/noahbenson/immlib"
47
+ documentation = "https://github.com/noahbenson/immlib"
48
+ repository = "https://github.com/noahbenson/immlib"
immlib-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ # immlib/__init__.py
4
+
5
+ '''``immlib`` is a library of tools for manipulating immutable scientific data.
6
+
7
+ The ``immlib`` library is designed to enable immutable data structures and lazy
8
+ computation in a scientific context, and it works primarily via a collection of
9
+ utility functions and through the use of decorators, which are generally
10
+ applied to classes and their members to declare how an immutable
11
+ data-structure's members are related. Taken together, these utilities form a
12
+ DSL-like system for declaring workflows and immutable data-structures with full
13
+ inheritance support.
14
+
15
+ Attributes
16
+ ----------
17
+ units : pint.UnitRegistry
18
+ The registry for units tracked by ``immlib``. The ``immlib.units`` object
19
+ is a global ``pint``-module unit registry that can be used as a single
20
+ global place for tracking units. Immlib functions that interact with units
21
+ generally take an argument ``ureg`` that can be used to modify this
22
+ registry. Additionally, the default registry (this object,
23
+ ``immlib.units``) can be temporarily changed in a local block using ``with
24
+ immlib.default_ureg(ureg): ...``.
25
+ version : immlib.Version
26
+ A representation of the ``immlib`` version. The version string may be
27
+ obtained via ``immlib.version.string``; major, minor, and micro numbers
28
+ (when present) may be obtained via ``immlib.version.major``,
29
+ ``immlib.version.minor``, and ``immlib.version.micro`` (when not provided
30
+ the are set to ``None``), and a stage tag (a string), if given, can be
31
+ obtained via ``immlib.version.stage``.
32
+ submodules : tuple of str
33
+ A tuple of strings, each of which is the name of one of the submodules in
34
+ ``immlib``. The modules are listed in load-order and all ``immlib``
35
+ submodules, including private submodules, are included.
36
+ docproc: docrep.DocstringProcessor object
37
+ This object is used to process all of the doc-strings in the ``immlib``
38
+ library; it should be used only with the ``immlib.docwrap`` decorator,
39
+ which can safely be applied anywhere in a sequence of decorators and which
40
+ correctly applies the ``wraps`` decorator to its argument. Function
41
+ documentation is always processed using the ``sections=('Parameters',
42
+ 'Returns', 'Raises', 'Examples', 'Inputs', 'Outputs')`` parameter and the
43
+ ``with_indent(4)`` decorator. The base-name for the function ``f`` is
44
+ ``f.__module__ + '.' + f.__name__``.
45
+ '''
46
+
47
+
48
+ # Imports #####################################################################
49
+
50
+ # We always load _init first.
51
+ from ._init import reclaim
52
+ # Then the core library.
53
+ from .doc import *
54
+ from .util import *
55
+ from .pathlib import *
56
+ from .iolib import *
57
+ from .workflow import *
58
+ from .types import *
59
+ # Import the Global UnitRegistry object to the global immlib scope. This is the
60
+ # value that gets updated when one runs `immlib.default_ureg()`, and this is
61
+ # the UnitRegistry that is used as the default registry for all ``immlib``
62
+ # functions.
63
+ from .util._quantity import _initial_global_ureg as units
64
+ # Do the same for the global DocstringProcessor (from the docrep library) from
65
+ # the doc subpackage.
66
+ from .doc._core import _initial_global_docproc as docproc
67
+ # We want the version object from the ._version namespace; this is always last.
68
+ from ._version import (version, Version)
69
+
70
+
71
+ # Modules/Reloading ###########################################################
72
+
73
+ submodules = (
74
+ 'immlib._init',
75
+ 'immlib.doc._core',
76
+ 'immlib.doc',
77
+ 'immlib.util._core',
78
+ 'immlib.util._numeric',
79
+ 'immlib.util._quantity',
80
+ 'immlib.util',
81
+ 'immlib.pathlib._osf',
82
+ 'immlib.pathlib._cache',
83
+ 'immlib.pathlib._core',
84
+ 'immlib.pathlib',
85
+ 'immlib.iolib._core',
86
+ 'immlib.iolib',
87
+ 'immlib.workflow._core',
88
+ 'immlib.workflow._plantype',
89
+ 'immlib.workflow',
90
+ 'immlib.types._core',
91
+ 'immlib.types',
92
+ 'immlib._version')
93
+ def reload_immlib():
94
+ """Reload and return the entire ``immlib`` package.
95
+
96
+ ``immlib.reload_immlib()`` reloads every submodule in the ``immlib``
97
+ package then reloads ``immlib`` itself, and returns the reloaded package.
98
+
99
+ .. Warning:: This function exists primarily for debugging purposes; its use
100
+ is not generally needed or advised by users of the library.
101
+
102
+ Returns
103
+ -------
104
+ module
105
+ The newly reloaded ``immlib`` module.
106
+
107
+ Examples
108
+ --------
109
+ >>> import immlib as il
110
+ >>> il.units = None # This will break parts of the library.
111
+ >>> il = il.reload_immlib() # But this resets it.
112
+ >>> il.units is not None
113
+ True
114
+ """
115
+ import sys, importlib
116
+ for mod in submodules:
117
+ importlib.reload(sys.modules[mod])
118
+ return importlib.reload(sys.modules[__name__])
119
+
120
+
121
+ # Package Meta-Code ###########################################################
122
+
123
+ __version__ = version.string
124
+ __all__ = tuple(
125
+ [k for k in locals()
126
+ if k[0] != '_'
127
+ if k != 'submodules'
128
+ if k != 'version'
129
+ if ('immlib.' + k) not in submodules])
130
+ # We want to mark our functions as being from the immlib module.
131
+ reclaim(__name__, __all__, del_reclaim=True)
@@ -0,0 +1,108 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ # immlib/_init.py
4
+
5
+ """Utilities and global definitions needed by theoretically the entire rest of
6
+ the `immlib` library during the import process.
7
+
8
+ The `immlib._init` module is the first submodule of the `immlib` library that
9
+ is loaded during the import process and is the firt that is reloaded when the
10
+ `immlib.reload_immlib()` library is called. Accordingly, functions and
11
+ variables that are needed by other modules can be put here. These functions and
12
+ variables, if they are intended to be public, should be reclaimed by another
13
+ module (see the `immlib._init.reclaim` / `immlib.util.reclaim` function).
14
+ """
15
+
16
+
17
+ # Dependencies ################################################################
18
+
19
+ import sys
20
+ from types import ModuleType
21
+
22
+
23
+ # Utility Functions ###########################################################
24
+
25
+ def reclaim(modname, attributes=Ellipsis, *,
26
+ del_reclaim=False,
27
+ skip_externs=True,
28
+ skip_private=True,
29
+ skip_modules=True):
30
+ """Sets the ``__module__`` attributes of objects in the module whose name
31
+ is `modname` to be `modname`.
32
+
33
+ ``reclaim(__name__, __all__)``, when run at the end of an ``__init__.py``
34
+ file, will set the module names of all objects in that module's ``__all__``
35
+ list to be ``__name__``, effectively hiding any submodule ownership of
36
+ those objects.
37
+
38
+ ``reclaim(__name__)`` attempts to reclaim all attributes in the module, not
39
+ just those in the ``__all__`` list.
40
+
41
+ Parameters
42
+ ----------
43
+ modname : str
44
+ The name of the module to reclaim. Typically, ``reclaim`` is called
45
+ from an ``__init__.py`` file using the syntax ``reclaim(__name__)`` or
46
+ ``reclaim(__name__, __all__)``.
47
+ attributes : Ellipsis | iterable of str, optional
48
+ The attributes to reclaim. If this is explicitly given then it must be
49
+ an iterable of attribute names (strings). In this case, no additional
50
+ checks implied by the subsequent ``skip_`` and ``only_`` parameters are
51
+ performed. If `attributes` is not provided or is given the value of
52
+ ``Ellipsis`` (the default), then all attributes of the module are
53
+ considered, and the filters specified by the subsequent options are
54
+ applied.
55
+ del_reclaim : bool, optional
56
+ If the optional argument `del_reclaim` is set to ``True``, then the
57
+ ``reclaim`` attribute is deleted from the module as well. This can be
58
+ used to automatically sanitize a module.
59
+ skip_externs : bool, optional
60
+ The `skip_externs` option (default: ``True``) can be set to ``False``
61
+ to instruct the ``reclaim`` function to include attributes that are not
62
+ from a submodule of the module named by `modname`.
63
+
64
+ If this option is set to ``True``, then attributes of the module on
65
+ which ``reclaim`` is run are only reclaimed if they belong to a
66
+ submodule of of that module.
67
+
68
+ .. Warning:: If `skip_externs` is set to ``False``, attributes like
69
+ ``np`` from the import statement ``import numpy as np`` at the top
70
+ of an ``__init__.py`` file can result in the ``numpy`` module being
71
+ reclaimed (if ``skip_modules`` is also ``False``). Similarly, if
72
+ the line ``from immlib.util import reclaim`` appears in an
73
+ ``__init__.py`` file followed by ``reclaim(__name__,
74
+ skip_externs=False)`` then the ``reclaim`` function will be
75
+ reclaimed by the module.
76
+ skip_private : boolean, optional
77
+ The `skip_private` option may be set to ``False`` to instruct the
78
+ function to claim ownership over attributes whose names begin with the
79
+ underscore (``_``) charcter. If this is not provided or is ``True``,
80
+ then such attributes are skipped.
81
+ skip_modules : boolean, optional
82
+ The `skip_modules` option (default: ``True``) may be set to false to
83
+ instruct the ``reclaim`` function to attempt to reclaim modules as well
84
+ as functions and attributes.
85
+
86
+ Returns
87
+ -------
88
+ str
89
+ The module named by the given `modname` argument.
90
+ """
91
+ mod = sys.modules[modname]
92
+ for k in dir(mod):
93
+ if skip_private and k[0] == '_':
94
+ continue
95
+ obj = getattr(mod, k)
96
+ if skip_modules and isinstance(obj, ModuleType):
97
+ continue
98
+ if del_reclaim and obj is reclaim:
99
+ continue
100
+ objmodname = getattr(obj, '__module__', modname)
101
+ if objmodname == modname:
102
+ continue
103
+ if skip_externs and not objmodname.startswith(modname):
104
+ continue
105
+ obj.__module__ = modname
106
+ if del_reclaim and hasattr(mod, 'reclaim'):
107
+ delattr(mod, 'reclaim')
108
+ return mod
@@ -0,0 +1,239 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ # immlib/_version.py
4
+
5
+
6
+ # Dependencies ################################################################
7
+
8
+ from ast import literal_eval
9
+ from collections import namedtuple
10
+ from pathlib import Path
11
+ from warnings import warn
12
+
13
+
14
+ # Version Type ################################################################
15
+
16
+ VersionTuple = namedtuple(
17
+ 'VersionTuple',
18
+ ['string', 'tuple', 'major', 'minor', 'micro', 'stage'])
19
+ class Version(VersionTuple):
20
+ """A type that represents a Python package version.
21
+
22
+ Python packages are represented simultaneously as version strings, version
23
+ tuples, and by the version components ``major``, ``minor``, ``micro``, and
24
+ ``stage``.
25
+
26
+ Parameters
27
+ ----------
28
+ string : str or None, optional
29
+ The version string to be represented. If this argument is not provided,
30
+ then one or both of the ``package_name`` and ``pyproject_path`` options
31
+ must be provided so that the version string can be obtained via the
32
+ package version or the ``pyproject.toml`` file.
33
+ package_name : str or None, optional
34
+ If the first argument (``string``) is provided, then this argument is
35
+ ignored; otherwise, the version string is first searched for by this
36
+ package name using the ``importlib`` or ``importlib_metadata``
37
+ packages. If found, then this version string is represented in the
38
+ ``Version`` object.
39
+ pyproject_path : path-like or None, optional
40
+ If the first argument (``string``) is not given and the
41
+ ``package_name`` is not given, then the version is searched for in the
42
+ ``pyproject.toml`` file given by this path. In order for such a file to
43
+ be valid, it must contain a line that, when stripped of whitespace,
44
+ begins with the string ``'version='`` followed by a string
45
+ representation (e.g., ``'version="1.12.5"'``). If such a line is found
46
+ in the ``[project]`` section of the TOM: file pointed to by this
47
+ argument, then it is represented as the version string in the
48
+ ``Version`` object.
49
+ on_error : {'warn' | 'ignore' | 'raise'}, optional
50
+ How to handle failures to deduce or parse the version number. If
51
+ ``'raise'`` is given, then the errors are allowed to be raised. If
52
+ ``'warn'``, then a warning is raised and a null version is returned. If
53
+ ``'ignore'``, then errors are ignored and a null version is
54
+ returned. The default is ``'raise'``.
55
+
56
+ tag_prefixes : tuple of str, optional
57
+ An optional tuple of strings that can appear as the prefixes of stage
58
+ tagss at the end of the version string. By default, this is ``('rc',
59
+ 'a', 'b')``, so version strings like ``'1.1.12a6'`` and ``'1.1.12rc6'``
60
+ are valid but ``'1.1.12c6'`` is not.
61
+
62
+ Attributes
63
+ ----------
64
+ string : str
65
+ The string representing the package version. For example ``"1.2.15"``
66
+ or ``"0.2.2.dev1"``.
67
+ tuple : tuple of int and str
68
+ The components of the version string, for example, ``(1, 2, 15)`` or
69
+ ``(0, 2, 2, 'dev1')``. Any missing component is excluded.
70
+ major : int
71
+ The major version number, typically indicates major API version.
72
+ minor : int
73
+ The minor version number, typically indicates minor API version.
74
+ micro : int
75
+ The micro version number, typically indicates patch increment number.
76
+ stage : str
77
+ The development stage of the version. For example ``'dev1'`` or
78
+ ``'rc2'``.
79
+ """
80
+
81
+ # Static Methods ----------------------------------------------------------
82
+ def getstring(package_name=None, pyproject_path=None):
83
+ """Returns the current version string for the given package name.
84
+
85
+ ``Version.getstring(package_name)`` returns the version string of the
86
+ package with the given package name.
87
+
88
+ ``Version.getstring(pyproject_path=path)`` returns the version string
89
+ found in the pyproject.toml file found at the given ``path``.
90
+
91
+ ``Version.getstring(package_name, path)`` returns
92
+ ``Version.getstring(package_name)`` if the given ``package_name`` is
93
+ found, otherwise returns ``Version.getstring(pyproject_path=path)``.
94
+ """
95
+ if package_name is None and pyproject_path is None:
96
+ raise ValueError("Version.getstring() requires 1 or 2 arguments")
97
+ if package_name is not None:
98
+ try:
99
+ from importlib.metadata import version
100
+ from importlib.metadata import PackageNotFoundError
101
+ except ModuleNotFoundError:
102
+ from importlib_metadata import version
103
+ from importlib_metadata import PackageNotFoundError
104
+ if pyproject_path is None:
105
+ return version(package_name)
106
+ # Try to deduce the version string but don't raise if this fails.
107
+ try:
108
+ return version(package_name)
109
+ except PackageNotFoundError:
110
+ pass
111
+ # Either a package name wasn't given or the package wasn't found; check
112
+ # the pyproject.toml if possible.
113
+ path = Path(pyproject_path)
114
+ with path.open('rt') as fl:
115
+ toml_lines = fl.read().split('\n')
116
+ in_project_section = False
117
+ for ln in toml_lines:
118
+ ln = ln.strip()
119
+ if ln == '[project]':
120
+ in_project_section = True
121
+ elif ln[0] == '[' and ln[-1] == ']':
122
+ in_project_section = False
123
+ elif in_project_section and '=' in ln:
124
+ parts = ln.split('=')
125
+ if parts[0].strip() == 'version':
126
+ v = '='.join(parts[1:]).strip()
127
+ return literal_eval(v)
128
+ # If we reach this point, we didn't fine a version line.
129
+ if package_name is not None:
130
+ raise RuntimeError(
131
+ f"Version.getstring() found no package named '{package_name}'"
132
+ f" and no 'version = ...' line in file {path}")
133
+ else:
134
+ raise RuntimeError(
135
+ f"Version.getstring() found no 'version = ...' line in file"
136
+ f" {path}")
137
+
138
+ # Construction ------------------------------------------------------------
139
+ __slots__ = ()
140
+ null = None
141
+ def __new__(cls, string=None, /, *,
142
+ package_name=None,
143
+ pyproject_path=None,
144
+ on_error='warn',
145
+ tag_prefixes=('rc', 'a', 'b')):
146
+ if on_error not in ('raise', 'warn', 'ignore'):
147
+ raise ValueError(
148
+ "invalid value for on_error; must be one of 'raise', 'warn',"
149
+ " or 'ignore'")
150
+ if string is None:
151
+ # Try to deduce the version string.
152
+ if on_error == 'raise':
153
+ string = Version.getstring(package_name, pyproject_path)
154
+ else:
155
+ try:
156
+ string = Version.getstring(package_name, pyproject_path)
157
+ except Exception as e:
158
+ if on_error == 'warn':
159
+ warn(
160
+ f"Version: could not deduce version string for"
161
+ f" package_name={repr(package_name)} and"
162
+ f" pyproject_path={repr(pyproject_path)}")
163
+ return Version.null
164
+ else:
165
+ return Version.null
166
+ ss = string.split('.')
167
+ nss = len(ss)
168
+ if nss == 4:
169
+ (major, minor, micro, stage) = ss
170
+ else:
171
+ if nss == 3:
172
+ (major, minor, micro) = ss
173
+ last = micro
174
+ elif nss == 2:
175
+ (major, minor) = ss
176
+ micro = '0'
177
+ last = minor
178
+ elif nss == 1:
179
+ major = ss
180
+ (minor, micro) = ('0', '0')
181
+ last = major
182
+ else:
183
+ raise ValueError(
184
+ f"invalid version string: '{string}' contains {nss}"
185
+ f" components")
186
+ stage = None
187
+ for tag in tag_prefixes:
188
+ if tag in last:
189
+ (num, stage) = last.split(tag)
190
+ stage = tag + stage
191
+ if last is micro:
192
+ micro = num
193
+ elif last is minor:
194
+ minor = num
195
+ else:
196
+ major = num
197
+ break
198
+ major = int(major)
199
+ minor = int(minor)
200
+ micro = int(micro)
201
+ tup = tuple(u for u in (major, minor, micro, stage) if u is not None)
202
+ return super(Version, cls).__new__(
203
+ cls,
204
+ string=string,
205
+ tuple=tup,
206
+ major=major,
207
+ minor=minor,
208
+ micro=micro,
209
+ stage=stage)
210
+ def __str__(self):
211
+ return self.string
212
+ def __repr__(self):
213
+ return f"Version({repr(self.string)})"
214
+ def __iter__(self):
215
+ return iter(self.tuple)
216
+ def __reversed__(self):
217
+ return reversed(self.tuple)
218
+ def __contains__(self, k):
219
+ if isinstance(k, str):
220
+ return k in self.string
221
+ else:
222
+ return k in self.tuple
223
+ Version.null = VersionTuple.__new__(
224
+ Version,
225
+ string='',
226
+ tuple=(),
227
+ major=None,
228
+ minor=None,
229
+ micro=None,
230
+ stage=None)
231
+
232
+
233
+ # Variables ###################################################################
234
+
235
+ # The path of immlib's pyproject.toml file.
236
+ pyproject_path = Path(__file__).parent.parent.parent / 'pyproject.toml'
237
+
238
+ # Declare the version object; this will automatically detect the version.
239
+ version = Version(package_name=__package__, pyproject_path=pyproject_path)