Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 3 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,6 @@ Python versions listed can be restricted by using the `--max`, `--min` and
`--compatible` options to the command. These roughly translate to `>=` for min, `<` for max
and `~=` for compatible in python version specifiers.

If you wish to find the latest binaries available from python.org for your platform
(or sources on Linux) there is the additional `online` command with some extra flags.

By default it will fetch the latest patches for each Python release (eg: 2.7.18 for 2.7) for
the hardware you're on. The filters for local versions also work.

* `--all-binaries` will get you all binary releases that match the restrictions.
* `--system` and `--machine` allow you to specify a platform other than the one you are using
(the values you give should match platform.system() and platform.machine() return values).
* `--prerelease` includes prerelease versions in the search.

Example:
`python pythonfinder.pyz online --min 3.10 --system Windows --machine AMD64`

```
| Python Version | URL |
| -------------- | ------------------------------------------------------------------ |
| 3.12.5 | https://www.python.org/ftp/python/3.12.5/python-3.12.5-amd64.exe |
| 3.11.9 | https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe |
| 3.10.11 | https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe |
```

## Library Usage ##

### Local installs ###
Expand Down Expand Up @@ -113,33 +91,13 @@ PythonInstall(version=(3, 13, 0, 'candidate', 1), executable='~\\.pyenv\\pyenv-w
There is now a submodule to search for virtual environments.

```python
import os
from ducktools.pythonfinder.venv import list_python_venvs

for venv in list_python_venvs():
for venv in list_python_venvs(recursive=False, search_parent_folders=True):
print(venv.executable)
```

### Python.org search ###

Python.org searches are handled by the `ducktools.pythonfinder.pythonorg_search` module.

```python
from packaging.specifiers import SpecifierSet
from ducktools.pythonfinder.pythonorg_search import PythonOrgSearch

# If system and machine are not provided this uses platform.system() and platform.machine()
searcher = PythonOrgSearch(system="Windows", machine="AMD64")

all_releases = searcher.releases
all_release_files = searcher.release_files
all_312_releases = searcher.matching_versions(SpecifierSet("~=3.12.0"))
all_312_downloads = searcher.matching_versions(SpecifierSet("~=3.12.0"))
all_312_311_win_binaries = searcher.all_matching_binaries(SpecifierSet(">=3.11.0, <3.13"))
latest_312_311_win_binaries = searcher.latest_minor_binaries(SpecifierSet(">=3.11.0, <3.13"))
latest_matching_win_binary = searcher.latest_binary_match(SpecifierSet(">=3.10"))
latest_prerelease_binary = searcher.latest_binary_match(SpecifierSet(">=3.10"), prereleases=True)
```

## Why? ##

For the purposes of PEP723 script dependencies and other releated tools
Expand All @@ -153,4 +111,4 @@ satisfy such a requirement.
That module appears to require searching for a specific version and will find venv pythons.

In contrast `ducktools.pythonfinder` simply yields python installs as they are discovered
and will attempt to avoid returning virtualenv python installs
and will attempt to avoid returning virtualenv python installs (unless you are searching for venvs)
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.10"
dependencies = [
"ducktools-lazyimporter>=0.7.3",
"ducktools-classbuilder>=0.9.1",
"packaging>=24.2",
"ducktools-lazyimporter>=0.8.4",
"ducktools-classbuilder>=0.13.1",
"packaging>=26.2",
]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down
94 changes: 3 additions & 91 deletions src/ducktools/pythonfinder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@
ModuleImport("subprocess"),
ModuleImport("sysconfig"),
ModuleImport("platform"),
FromImport(".pythonorg_search", "PythonOrgSearch"),
FromImport("packaging.specifiers", "SpecifierSet"),
FromImport("urllib.error", "URLError"),
],
globs=globals()
)
Expand Down Expand Up @@ -137,39 +135,9 @@ def get_parser() -> argparse.ArgumentParser:
help="Clear the cache of Python install details"
)

online = subparsers.add_parser(
"online",
help="Get links to binaries from python.org"
)

# Shared arguments
for p in [parser, online]:
p.add_argument("--min", help="Specify minimum Python version")
p.add_argument("--max", help="Specify maximum Python version")
p.add_argument("--compatible", help="Specify compatible Python version")

online.add_argument(
"--all-binaries",
action="store_true",
help="Provide *all* matching binaries and "
"not just the latest minor versions"
)
online.add_argument(
"--system",
action="store",
help="Get python installers for a different system (eg: Windows, Darwin, Linux)"
)
online.add_argument(
"--machine",
action="store",
help="Get python installers for a different architecture (eg: AMD64, ARM64, x86)"
)

online.add_argument(
"--prerelease",
action="store_true",
help="Include prerelease versions"
)
parser.add_argument("--min", help="Specify minimum Python version")
parser.add_argument("--max", help="Specify maximum Python version")
parser.add_argument("--compatible", help="Specify compatible Python version")

return parser

Expand Down Expand Up @@ -263,47 +231,6 @@ def display_local_installs(
print(f"| {version_str:>{max_version_len}s} | {executable:<{max_executable_len}s} |")


def display_remote_binaries(
min_ver: str,
max_ver: str,
compatible: str,
all_binaries: bool,
system: str,
machine: str,
prerelease: bool,
) -> None:
specs = []
if min_ver:
specs.append(f">={min_ver}")
if max_ver:
specs.append(f"<{max_ver}")
if compatible:
specs.append(f"~={compatible}")

spec = _laz.SpecifierSet(",".join(specs))

searcher = _laz.PythonOrgSearch(system=system, machine=machine)
if all_binaries:
releases = searcher.all_matching_binaries(spec, prereleases=prerelease)
else:
releases = searcher.latest_minor_binaries(spec, prereleases=prerelease)

headings = ["Python Version", "URL"]

if releases:
max_url_len = max(
len(headings[1]), max(len(release.url) for release in releases)
)
headings_str = f"| {headings[0]} | {headings[1]:<{max_url_len}s} |"

print(headings_str)
print(f"| {'-' * len(headings[0])} | {'-' * max_url_len} |")

for release in releases:
print(f"| {release.version:>14s} | {release.url:<{max_url_len}s} |")
else:
print("No Python releases found matching specification")


def main() -> int:
if sys.version_info < (3, 10):
Expand All @@ -319,21 +246,6 @@ def main() -> int:

if vals.command == "clear-cache":
purge_caches()
elif vals.command == "online":
system = vals.system if vals.system else _laz.platform.system()
machine = vals.machine if vals.machine else _laz.platform.machine()
try:
display_remote_binaries(
vals.min,
vals.max,
vals.compatible,
vals.all_binaries,
system,
machine,
vals.prerelease,
)
except _laz.URLError:
print("Could not connect to python.org")
else:
display_local_installs(
min_ver=vals.min,
Expand Down
Loading