ci-helper 0.1.0__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.
ci_helper/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ try:
2
+ from .__version__ import __version__
3
+ except ImportError:
4
+ __version__ = None
ci_helper/__main__.py ADDED
@@ -0,0 +1,120 @@
1
+ import sys
2
+ from pathlib import Path
3
+ import argparse
4
+ import json
5
+ import subprocess
6
+ import requests
7
+
8
+ from .__version__ import __version__
9
+
10
+
11
+ # Command line args that can be used in place of "setup.py" for projects that lack a
12
+ # setup.py, runs a minimal setup.py similar to what pip does for projects with no
13
+ # setup.py.
14
+ _SETUP_PY_STUB = [
15
+ "-c",
16
+ 'import sys, setuptools; sys.argv[0] = __file__ = "setup.py"; setuptools.setup()',
17
+ ]
18
+
19
+
20
+ def setup_py(project_dir):
21
+ """Returns a list of command line arguments to be used in place of ["setup.py"]. If
22
+ setup.py exists, then this is just ["setup.py"]. Otherwise, if setup.cfg or
23
+ pyproject.toml exists, returns args that pass a code snippet to Python with "-c" to
24
+ execute a minimal setup.py calling setuptools.setup(). If none of pyproject.toml,
25
+ setup.cfg, or setup.py exists, raises an exception."""
26
+ if Path(project_dir, 'setup.py').exists():
27
+ return ['setup.py']
28
+ elif any(Path(project_dir, s).exists() for s in ['setup.cfg', 'pyproject.toml']):
29
+ return _SETUP_PY_STUB
30
+ msg = f"""{project_dir} does not look like a python project directory: contains no
31
+ setup.py, setup.cfg, or pyproject.toml"""
32
+ raise RuntimeError(' '.join(msg.split()))
33
+
34
+
35
+ def get_pythons():
36
+ """Return stable, non-end-of-life Python versions in X.Y format"""
37
+ URL = "https://raw.githubusercontent.com/python/devguide/refs/heads/main/include/release-cycle.json"
38
+ response = requests.get(URL, timeout=30)
39
+ if not response.ok:
40
+ raise ValueError(f"{response.status_code} {response.reason}")
41
+ pythons = response.json()
42
+ pythons = [p for p in pythons if pythons[p]['status'] in ('bugfix', 'security')]
43
+ pythons.sort(key=lambda ver: [int(part) for part in ver.split('.')])
44
+ return pythons
45
+
46
+
47
+ def main():
48
+ # Since setuptools_conda is self-hosting, it needs toml and distlib to read its own
49
+ # requirements just to know that it needs to install toml and distlib! So bootstrap
50
+ # that up if necessary.
51
+
52
+ parser = argparse.ArgumentParser(prog='ci-helper')
53
+ parser.add_argument(
54
+ '--version',
55
+ action='version',
56
+ version=__version__,
57
+ )
58
+ subparsers = parser.add_subparsers(
59
+ dest="command", required=True, help="Action to perform"
60
+ )
61
+ parser_pythons = subparsers.add_parser(
62
+ "pythons",
63
+ help="Output list of stable Python versions that have not yet reached end of "
64
+ + "life, see `ci-helper pythons -h`",
65
+ )
66
+ parser_pythons.add_argument(
67
+ "--cibw",
68
+ action="store_true",
69
+ help="Output as a space-separated list in the format "
70
+ + "`cpXY-* cpXY-*` as appropriate for the CIBW_BUILD environment variable "
71
+ + "to build for all stable CPython versions, otherwise versions are output as "
72
+ "a comma-separated list in the format X.Y,X.Y",
73
+ )
74
+ _ = subparsers.add_parser(
75
+ "defaultpython",
76
+ help="Output the second-latest stable Python version in X.Y format, "
77
+ "useful as a good choice for a default python version",
78
+ )
79
+ parser_distinfo = subparsers.add_parser(
80
+ "distinfo",
81
+ help="Output info about the distribution, see `ci-helper distinfo -h`",
82
+ )
83
+ parser_distinfo.add_argument(
84
+ "field",
85
+ choices=['name', 'version', 'is_pure', 'has_env_markers'],
86
+ nargs="?",
87
+ help="Name of field to output as a single json value, "
88
+ + "if not given, all info is output as json",
89
+ )
90
+
91
+ args = parser.parse_args()
92
+
93
+ if args.command == 'distinfo':
94
+ # distinfo_args = parser_distinfo.parse_args()
95
+ cmd = [sys.executable, *setup_py('.'), '-q', 'ci_distinfo']
96
+ result = subprocess.run(cmd, check=True, capture_output=True)
97
+ info = result.stdout.decode('utf8')
98
+ if args.field is not None:
99
+ info = json.loads(info)
100
+ value = info[args.field]
101
+ if isinstance(value, str):
102
+ print(value)
103
+ else:
104
+ print(json.dumps(value))
105
+ else:
106
+ print(info)
107
+ elif args.command == 'pythons':
108
+ pythons = get_pythons()
109
+ if args.cibw:
110
+ print(' '.join([f"cp{p.replace('.', '')}-*" for p in pythons]))
111
+ else:
112
+ print(','.join(pythons))
113
+ elif args.command == 'defaultpython':
114
+ pythons = get_pythons()
115
+ print(pythons[-2])
116
+ sys.exit(0)
117
+
118
+
119
+ if __name__ == '__main__':
120
+ main()
@@ -0,0 +1,21 @@
1
+ import os
2
+ from pathlib import Path
3
+ try:
4
+ import importlib.metadata as importlib_metadata
5
+ except ImportError:
6
+ import importlib_metadata
7
+
8
+ VERSION_SCHEME = {
9
+ "version_scheme": os.getenv("SCM_VERSION_SCHEME", "guess-next-dev"),
10
+ "local_scheme": os.getenv("SCM_LOCAL_SCHEME", "node-and-date"),
11
+ }
12
+
13
+ root = Path(__file__).parent.parent
14
+ if (root / '.git').is_dir():
15
+ from setuptools_scm import get_version
16
+ __version__ = get_version(root, **VERSION_SCHEME)
17
+ else:
18
+ try:
19
+ __version__ = importlib_metadata.version(__package__)
20
+ except importlib_metadata.PackageNotFoundError:
21
+ __version__ = None
@@ -0,0 +1,66 @@
1
+ from setuptools import Command
2
+ import json
3
+ from pathlib import Path
4
+
5
+
6
+ def get_pyproject_toml_entry(proj, *keys):
7
+ """Return [build-system] requires as read from proj/pyproject.toml, if any"""
8
+ # this import is here to avoid bootstrapping issues when building wheels for this
9
+ # package itself
10
+ import toml
11
+ pyproject_toml = Path(proj, 'pyproject.toml')
12
+ if not pyproject_toml.exists():
13
+ return None
14
+ config = toml.load(pyproject_toml)
15
+ try:
16
+ for key in keys:
17
+ config = config[key]
18
+ return config
19
+ except KeyError:
20
+ return None
21
+
22
+
23
+ def has_environment_markers(setup_requires, install_requires, extras_requires):
24
+ """Given a list of install_requires and a dict of extras_requires, return if there
25
+ are any environment markers"""
26
+ for item in install_requires:
27
+ if ';' in item:
28
+ return True
29
+ for key in extras_requires:
30
+ if key.startswith(':'):
31
+ # an extras_requires item being used as an environment marker, an old
32
+ # pattern allowed by setuptools
33
+ return True
34
+ return False
35
+
36
+
37
+ class ci_distinfo(Command):
38
+ description = "Get package info useful for building on CI"
39
+ user_options = []
40
+
41
+ def initialize_options(self):
42
+ pass
43
+
44
+ def finalize_options(self):
45
+ pass
46
+
47
+ def run(self):
48
+
49
+ setup_requires = get_pyproject_toml_entry('.', 'build-system', 'requires')
50
+ if setup_requires is None:
51
+ setup_requires = self.distribution.setup_requires
52
+
53
+ info = {
54
+ 'name': self.distribution.get_name(),
55
+ 'version': self.distribution.get_version(),
56
+ 'is_pure': not (
57
+ self.distribution.has_ext_modules()
58
+ or self.distribution.has_c_libraries()
59
+ ),
60
+ 'has_env_markers': has_environment_markers(
61
+ setup_requires,
62
+ self.distribution.install_requires,
63
+ self.distribution.extras_require,
64
+ ),
65
+ }
66
+ print(json.dumps(info, indent=4))
@@ -0,0 +1 @@
1
+ from ci_helper.ci_distinfo import ci_distinfo
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Christopher Billington
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.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.1
2
+ Name: ci-helper
3
+ Version: 0.1.0
4
+ Summary: Get info on how a package should be built on CI
5
+ Home-page: http://github.com/chrisjbillington/ci-helper
6
+ Author: Christopher Billington
7
+ Author-email: chrisjbillington@gmail.com
8
+ License: MIT
9
+ Project-URL: Source Code, https://github.com/chrisjbillington/ci-helper
10
+ Project-URL: Download, https://github.com/chrisjbillington/ci-helper/releases
11
+ Project-URL: Tracker, https://github.com/chrisjbillington/ci-helper/issues
12
+ Keywords: build setuptools conda
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: setuptools_scm
19
+ Requires-Dist: importlib_metadata
20
+ Requires-Dist: toml
21
+ Requires-Dist: requests
22
+
23
+ # ci-helper
24
+
25
+ Output information to help build a Python package in CI
26
+
27
+ ## Motivation
28
+
29
+ This tool exists to address certain issues of bit-rot and deployment of GitHub Actions
30
+ workflows (though the problem exists for CI generally) that build Python packages.
31
+
32
+ It looks up what current Python versions are supported so you can target them in your
33
+ builds without having to maintain a curated list. It determines whether building on
34
+ multiple OSs or Python versions is needed for a Python package, allowing you to deploy a
35
+ generic CI configuration for different types of packages without having to modify it for
36
+ each one specifically when they only vary in this way.
37
+
38
+ Specifically:
39
+
40
+ * It lists all stable and supported Python versions so your CI can target all supported
41
+ versions of Python without having to curate a manual list.
42
+
43
+ * It tells you what the second-most-recent minor version of Python is, so you can use that
44
+ as a sensible default for tools that require a recent-ish Python but are likely not to
45
+ work immediately after a new Python is released.
46
+
47
+ * It tells you whether a Python package is pure Python or not, so that your CI knows
48
+ whether it needs to build on multiple OSs/Python versions.
49
+
50
+ * It tells you whether a Python package's dependencies have environment markers or not, so
51
+ that your CI knows whether it needs to build on multiple OSs/Python versions, for
52
+ package formats that don't support environment markers in dependencies (e.g. conda
53
+ packages).
54
+
55
+ These functions are all serving the goals of:
56
+
57
+ * Minimising how often you need to modify your CI configs just because a new version of
58
+ Python came our or an old version reached end-of-life
59
+ * Minimising (ideally to zero) how much you need to customise an otherwise generic CI
60
+ config when you re-use it for different types of Python packages (e.g. pure vs impure)
61
+
62
+ ## Installation
63
+
64
+ This package is available on PyPI, install it to your current Python environemnt with
65
+ ```bash
66
+ pip install ci-helper
67
+ ```
68
+
69
+ ## Usage:
70
+
71
+ ```bash
72
+ # The second-most recent minor Python release, a good choice for the version to run
73
+ # tools from:
74
+ $ ci-helper defaultpython
75
+ 3.12
76
+
77
+ # All stable, non-end-of-life Python versions:
78
+ $ ci-helper pythons
79
+ 3.9,3.10,3.11,3.12,3.13
80
+
81
+ # Same but in the format used by `cibuildwheel`'s `CIBW_BUILD` environment variable (cpython-only for now):
82
+ $ ci-helper pythons --cibw
83
+ cp39-* cp310-* cp311-* cp312-* cp313-*
84
+
85
+ # Info about the source Python project in the current working directory - name, version,
86
+ # whether it's a pure Python package, and whether its build or run requirements contain
87
+ # any # environment markers (that is, whether its requirements vary by platform or
88
+ # Python version):
89
+ $ ci-helper distinfo
90
+ {
91
+ "name": "ci-helper",
92
+ "version": "0.1.dev1+g5043fb5.d20241202",
93
+ "is_pure": true,
94
+ "has_env_markers": false
95
+ }
96
+
97
+ # Same but one field at a time, more convenient for assigning to environment variables:
98
+ $ ci-helper distinfo name
99
+ ci-helper
100
+ $ ci-helper distinfo version
101
+ 0.1.0
102
+ $ ci-helper distinfo is_pure
103
+ true
104
+ $ ci-helper distinfo has_env_markers
105
+ false
106
+ ```
107
+
108
+ ## Full help text
109
+
110
+ ```shell
111
+ $ ci-helper -h
112
+ usage: ci-helper [-h] [--version] {pythons,defaultpython,distinfo} ...
113
+
114
+ positional arguments:
115
+ {pythons,defaultpython,distinfo}
116
+ Action to perform
117
+ pythons Output list of stable Python versions that have not yet reached end of life, see `ci-helper pythons -h`
118
+ defaultpython Output the second-latest stable Python version in X.Y format, useful as a good choice for a default python version
119
+ distinfo Output info about the distribution, see `ci-helper distinfo -h`
120
+
121
+ options:
122
+ -h, --help show this help message and exit
123
+ --version show program's version number and exit
124
+ ```
125
+
126
+ ```shell
127
+ $ ci-helper pythons -h
128
+ usage: ci-helper pythons [-h] [--cibw]
129
+
130
+ options:
131
+ -h, --help show this help message and exit
132
+ --cibw Output as a space-separated list in the format `cpXY-* cpXY-*` as appropriate for the CIBW_BUILD environment variable to build for all
133
+ stable CPython versions, otherwise versions are output as a comma-separated list in the format X.Y,X.Y
134
+ ```
135
+
136
+ ```shell
137
+ $ ci-helper defaultpython -h
138
+ usage: ci-helper defaultpython [-h]
139
+
140
+ options:
141
+ -h, --help show this help message and exit
142
+ ```
143
+
144
+ ```shell
145
+ $ ci-helper distinfo -h
146
+ usage: ci-helper distinfo [-h] [{name,version,is_pure,has_env_markers}]
147
+
148
+ positional arguments:
149
+ {name,version,is_pure,has_env_markers}
150
+ Name of field to output as a single json value, if not given, all info is output as json
151
+
152
+ options:
153
+ -h, --help show this help message and exit
154
+ ```
155
+
@@ -0,0 +1,11 @@
1
+ ci_helper/__init__.py,sha256=npB_KdSACjRyJBy26TlLH10bIQfUOiuSsY3qfbOPsrE,89
2
+ ci_helper/__main__.py,sha256=fbOQhtWVfi4kBLd4d9YJT158NDZ_fgn_23uiA3RQ5gk,4421
3
+ ci_helper/__version__.py,sha256=PKz6bKiuM7wAkBz7aPv3HGZ4P_H59_GqQ8O4b7vSYTg,624
4
+ ci_helper/ci_distinfo.py,sha256=aTfWgQ1mq2dNBW7IYv0Tv0DBogMyb5zgV2nf7sayc8M,2052
5
+ ci_helper-0.1.0.data/data/lib/python3.12/site-packages/setuptools/_distutils/command/ci_distinfo.py,sha256=wv41yjVJHfhgcplPzgA0SHRZjoGyiApDwJrI_9DK7tw,46
6
+ ci_helper-0.1.0.dist-info/LICENSE.txt,sha256=3mgN7gts5RUJpaxp4BZ8quNCSpZGbtsFib10aRfwGHc,1079
7
+ ci_helper-0.1.0.dist-info/METADATA,sha256=SDSACFVsEsgryXWDzg844UMN2v_rs60RTc1jZ87A1nY,5457
8
+ ci_helper-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
9
+ ci_helper-0.1.0.dist-info/entry_points.txt,sha256=9elbjJQ006m9owk3zYufl-CMqaL9F5cgHVDeJcNFxwE,54
10
+ ci_helper-0.1.0.dist-info/top_level.txt,sha256=hDnJJ-LWQDv6CsxZ0T4b69G4yfSND5EofhlgMu8QrfU,10
11
+ ci_helper-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ci-helper = ci_helper.__main__:main
@@ -0,0 +1 @@
1
+ ci_helper