Skip to content

Commit 2adedd7

Browse files
committed
Initial release
0 parents  commit 2adedd7

File tree

9 files changed

+722
-0
lines changed

9 files changed

+722
-0
lines changed

.gitattributes

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Enforce Unix line endings
2+
* text=auto eol=lf
3+
4+
# Explicitly declare text files you want to always be normalized and converted
5+
# to native line endings on checkout.
6+
*.css text
7+
*.js text
8+
*.html text
9+
*.py text
10+
*.c text
11+
*.h text
12+
*.cpp text
13+
*.hpp text
14+
*.cu text
15+
*.sh text
16+
17+
# Denote all files that are truly binary and should not be modified.
18+
*.bin binary
19+
*.npy binary
20+
*.npz binary
21+
*.pkl binary
22+
*.pth binary
23+
*.cu.o binary
24+
*.so binary
25+
*.png binary
26+
*.jpg binary
27+
*.zip binary
28+
*.7z binary
29+
*.gz binary

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024 Cyrus Leung
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Python Compatibility Matrix
2+
3+
`pycm` is a command-line tool for checking which versions of Python are supported by a given package.
4+
5+
## Installation
6+
7+
### Requirements
8+
9+
- [Python](https://www.python.org/) 3.8+
10+
11+
### Procedure
12+
13+
1. Run `python -m pip install git+https://github.com/DarkLight1337/pycm.git` to install the package from this repository.
14+
15+
## Usage
16+
17+
### Syntax
18+
19+
```
20+
pycm <package> --python=<versions>
21+
```
22+
23+
### Example
24+
25+
Show the versions of [TensorFlow](https://tensorflow.org/) that can be used with Python 3.8-3.11:
26+
27+
```
28+
> pycm tensorflow --python=3.8,3.9,3.10,3.11
29+
tensorflow (→) 2.2.3 2.3.4 2.4.4 2.5.3 2.6.5 2.7.4 2.8.4 2.9.3 2.10.1 2.11.1 2.12.1 2.13.1 2.14.1 2.15.1 2.16.1
30+
Python (↓)
31+
3.8 Y Y Y Y Y Y Y Y Y Y Y Y
32+
3.9 Y Y Y Y Y Y Y Y Y Y Y Y
33+
3.10 Y Y Y Y Y Y Y Y Y
34+
3.11 Y Y Y Y Y
35+
```
36+
37+
(You can cross-check this table against [the official compatibility matrix](https://www.tensorflow.org/install/source#tested_build_configurations).)
38+
39+
## Development
40+
41+
### Requirements
42+
43+
- [Python](https://www.python.org/) 3.8+
44+
- [Poetry](https://python-poetry.org/) 1.4+
45+
46+
### Setup
47+
48+
1. Clone this repository to your machine.
49+
2. Run `poetry lock --no-update && poetry install --with dev` to setup the Python enviroment.
50+
51+
### Lint
52+
53+
1. [Setup](#setup) the development environment.
54+
2. Run `poetry run deptry . && poetry run ruff check . && poetry run pyright .` to lint the code.
55+

poetry.lock

Lines changed: 351 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pycm/__init__.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
from collections import defaultdict
4+
from collections.abc import Collection
5+
from itertools import groupby
6+
import json
7+
import re
8+
from urllib.request import urlopen
9+
10+
import click
11+
import pandas as pd
12+
from pkg_resources import Requirement, parse_version
13+
14+
__all__ = ['cli']
15+
16+
# Dummy requirement that cannot be satisfied
17+
BAD_REQUIREMENT = Requirement.parse('python<0,>0')
18+
19+
def _get_requires_python_constraint(dist_data: dict[str, str]) -> Requirement | None:
20+
requires_python_data: str | None = dist_data.get('requires_python')
21+
if requires_python_data is None:
22+
return None
23+
24+
return Requirement.parse(f'python{requires_python_data}')
25+
26+
def _get_python_requirements(dist_data: dict[str, str]) -> Collection[Requirement]:
27+
requires_python_constraint = _get_requires_python_constraint(dist_data)
28+
29+
python_version_data: str | None = dist_data.get('python_version')
30+
if python_version_data is None or python_version_data == 'source':
31+
return [] if requires_python_constraint is None else [requires_python_constraint]
32+
33+
m = re.match(r'[a-z]{2}([0-9])([0-9]*)', python_version_data)
34+
if m is None:
35+
return [BAD_REQUIREMENT] if requires_python_constraint is None else [requires_python_constraint]
36+
37+
major, minor = m.groups()
38+
if minor:
39+
bdist_constraint = Requirement.parse(f'python=={major}.{minor}.*')
40+
else:
41+
bdist_constraint = Requirement.parse(f'python=={major}.*')
42+
43+
if requires_python_constraint is None:
44+
return [bdist_constraint]
45+
else:
46+
return [requires_python_constraint, bdist_constraint]
47+
48+
@click.command()
49+
@click.argument('package_name', type=str)
50+
@click.option('--python', 'python_versions', metavar='<VERSIONS>', type=str, required=True, help='A comma-separated list of Python versions to check against')
51+
def cli(package_name: str, python_versions: str) -> None:
52+
"""Show the versions of a PyPI package that are compatible with each Python version."""
53+
python_versions_lst = python_versions.split(',')
54+
output = defaultdict(lambda: defaultdict(lambda: ''))
55+
56+
data = json.load(urlopen(f'https://pypi.org/pypi/{package_name}/json'))
57+
for package_version, release_data in data['releases'].items():
58+
for dist_data in release_data:
59+
python_requirements = _get_python_requirements(dist_data)
60+
61+
for python_version in python_versions_lst:
62+
if all(python_version in r for r in python_requirements):
63+
output[python_version][package_version] = 'Y'
64+
65+
output_df = pd.DataFrame.from_dict(data=output, orient='index').fillna('')
66+
67+
# We only care about the latest patch version for each minor version
68+
output_columns = tuple(str(c) for c in output_df.columns)
69+
latest_patches = [
70+
max(columns, key=parse_version)
71+
for _, columns in groupby(output_columns, key=lambda s: parse_version(s).release[:2])
72+
]
73+
74+
formatted_output_df = output_df \
75+
.loc[sorted(output_df.index, key=parse_version), sorted(latest_patches, key=parse_version)] \
76+
.rename_axis(index='Python (↓)', columns=f'{package_name} (→)')
77+
78+
click.echo(formatted_output_df)

pycm/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import cli
2+
3+
cli()

pycm/py.typed

Whitespace-only changes.

pyproject.toml

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
[build-system]
2+
requires = ["poetry-core"]
3+
build-backend = "poetry.core.masonry.api"
4+
5+
[tool.poetry]
6+
name = "pycm"
7+
version = "1.0.0"
8+
description = "Command-line tool for checking Python compatibility"
9+
license = "MIT"
10+
authors = [
11+
"Cyrus Leung <[email protected]>",
12+
]
13+
homepage = "https://github.com/DarkLight1337/pycm"
14+
repository = "https://github.com/DarkLight1337/pycm"
15+
keywords = [
16+
"cli", "dependencies"
17+
]
18+
classifiers = [
19+
"License :: OSI Approved :: MIT License",
20+
"Software Development :: Libraries :: Python Modules",
21+
]
22+
23+
[tool.poetry.scripts]
24+
pycm = "pycm:cli"
25+
26+
[tool.poetry.dependencies]
27+
python = "^3.8"
28+
29+
click = ">=8.1"
30+
pandas = ">=2.0"
31+
32+
[tool.poetry.group.dev]
33+
optional = true
34+
35+
[tool.poetry.group.dev.dependencies]
36+
deptry = ">=0.14"
37+
ruff = ">=0.3"
38+
pyright = ">=1.1.354"
39+
40+
[tool.deptry]
41+
extend_exclude = ["__pycache__"]
42+
43+
[tool.deptry.per_rule_ignores]
44+
DEP001 = ["_typeshed", "pkg_resources"]
45+
46+
[tool.ruff]
47+
line-length = 100
48+
target-version = "py38"
49+
50+
[tool.ruff.lint]
51+
select = [
52+
# Pyflakes
53+
"F",
54+
# pycodestyle
55+
"E", "W",
56+
# flake8-boolean-trap
57+
"FBT",
58+
# flake8-bugbear
59+
"B",
60+
# flake8-builtins
61+
"A",
62+
# flake8-commas
63+
"COM",
64+
# flake8-comprehensions
65+
"C4",
66+
# flake8-datetimez
67+
"DTZ",
68+
# flake8-debugger
69+
"T10",
70+
# flake8-errmsg
71+
"EM",
72+
# flake8-future-annotations
73+
"FA",
74+
# flake8-implicit-str-concat
75+
"ISC",
76+
# flake8-import-conventions
77+
"ICN",
78+
# flake8-logging
79+
"LOG",
80+
# flake8-logging-format
81+
"G",
82+
# flake8-no-pep420
83+
"INP",
84+
# flake8-pie
85+
"PIE",
86+
# flake8-print
87+
"T20",
88+
# flake8-raise
89+
"RSE",
90+
# flake8-simplify
91+
"SIM",
92+
# flake8-use-pathlib
93+
"PTH",
94+
# NumPy-specific rules
95+
"NPY",
96+
# Pandas-specific rules
97+
"PD",
98+
# Ruff-specific rules
99+
"RUF",
100+
# isort
101+
"I",
102+
# pydocstyle
103+
"D",
104+
# pyupgrade
105+
"UP",
106+
]
107+
ignore = [
108+
"F403", "F405",
109+
"E501",
110+
"SIM102", "SIM117", "SIM118",
111+
"PIE790",
112+
"D1", "D205", "D401",
113+
]
114+
115+
[tool.ruff.lint.per-file-ignores]
116+
"__init__.py" = ["F401"]
117+
118+
[tool.ruff.lint.flake8-builtins]
119+
builtins-ignorelist = ["copyright", "hash", "id", "type"]
120+
121+
[tool.ruff.lint.flake8-comprehensions]
122+
allow-dict-calls-with-keyword-arguments = true
123+
124+
[tool.ruff.lint.isort]
125+
classes = [
126+
# Standard library
127+
"ABC", "IO",
128+
]
129+
extra-standard-library = ["typing_extensions"]
130+
131+
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
132+
combine-as-imports = true
133+
force-sort-within-sections = true
134+
135+
[tool.ruff.lint.pydocstyle]
136+
convention = "numpy"
137+
138+
[tool.ruff.lint.pyupgrade]
139+
keep-runtime-typing = true
140+
141+
[tool.pyright]
142+
# These are set to "error" in "strict" mode
143+
reportConstantRedefinition = "warning"
144+
reportDeprecated = "warning"
145+
reportDuplicateImport = "warning"
146+
reportFunctionMemberAccess = "warning"
147+
reportIncompatibleMethodOverride = "warning"
148+
reportIncompatibleVariableOverride = "warning"
149+
reportIncompleteStub = "warning"
150+
reportInconsistentConstructor = "warning"
151+
reportInvalidStubStatement = "warning"
152+
reportMatchNotExhaustive = "warning"
153+
reportMissingParameterType = "warning"
154+
reportMissingTypeArgument = "warning"
155+
reportOverlappingOverload = "warning"
156+
reportPrivateUsage = "warning"
157+
reportTypeCommentUsage = "warning"
158+
# reportUnknownArgumentType = "warning"
159+
# reportUnknownLambdaType = "warning"
160+
# reportUnknownMemberType = "warning"
161+
# reportUnknownParameterType = "warning"
162+
# reportUnknownVariableType = "warning"
163+
reportUnnecessaryCast = "warning"
164+
reportUnnecessaryComparison = "warning"
165+
reportUnnecessaryContains = "warning"
166+
reportUnnecessaryIsInstance = "warning"
167+
# reportUnusedClass = "warning"
168+
reportUnusedImport = "warning"
169+
# reportUnusedFunction = "warning"
170+
reportUnusedVariable = "warning"
171+
reportUntypedBaseClass = "warning"
172+
reportUntypedClassDecorator = "warning"
173+
reportUntypedFunctionDecorator = "warning"
174+
reportUntypedNamedTuple = "warning"
175+
176+
# These are set to "none" even in "strict" mode
177+
reportCallInDefaultInitializer = "warning"
178+
# reportImplicitOverride = "warning"
179+
reportImplicitStringConcatenation = "warning"
180+
# reportImportCycles = "warning"
181+
reportMissingSuperCall = "warning"
182+
reportPropertyTypeMismatch = "warning"
183+
reportShadowedImports = "warning"
184+
reportUninitializedInstanceVariable = "warning"
185+
reportUnnecessaryTypeIgnoreComment = "information"
186+
# reportUnusedCallResult = "warning"

0 commit comments

Comments
 (0)