-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
GH-107956: install a static build description file #108483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5031753
4da5996
4047d86
b71fab2
de5729e
4155f2c
4adb942
d1eaf10
5b3fe21
7e7c59a
f792b64
b8d6aed
15f8a1a
c96e447
217682b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,4 @@ interpreter and things that make working with Python easier. | |
windows.rst | ||
mac.rst | ||
editors.rst | ||
introspection.rst |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
.. _introspection: | ||
|
||
*********************************** | ||
Introspecting a Python installation | ||
*********************************** | ||
|
||
The mechanisms available to introspect a Python installation are highly | ||
dependent on the installation you have. Their availability may depend on the | ||
Python implementation in question, the build format, the target platform, or | ||
several other details. | ||
|
||
This document aims to give an overview of these mechanisms and provide the user | ||
a general guide on how to approach this task, but users should still refer to | ||
the documentation of their installation for more accurate information regarding | ||
implementation details and availability. | ||
|
||
|
||
Introduction | ||
============ | ||
|
||
Generally, the most portable way to do this is by running some Python code to | ||
extract information from the target interpreter, though this can often be | ||
automated with the use of built-in helpers (see | ||
:ref:`introspection-stdlib-clis`, :ref:`introspection-python-config-script`). | ||
|
||
However, when introspecting a Python installation, running code is often | ||
undesirable or impossible. For this purpose, Python defines a standard | ||
:ref:`static install description file <introspection-install-details>` (please | ||
see the :ref:`introspection-install-details-format` section for the format | ||
specification), which may be provided together with your interpreter (please | ||
see the :ref:`introspection-install-details-location` section for more details). | ||
When available, this file can be used to lookup certain details from the | ||
installation in question without having to run the interpreter. | ||
|
||
|
||
Reference | ||
========= | ||
|
||
|
||
.. _introspection-install-details: | ||
|
||
Install details file | ||
-------------------- | ||
|
||
|
||
.. _introspection-install-details-format: | ||
|
||
Format | ||
~~~~~~ | ||
|
||
The details file is JSON file and the top-level object is a dictionary | ||
containing the data described in :ref:`introspection-install-details-fields`. | ||
|
||
|
||
.. _introspection-install-details-fields: | ||
|
||
Fields | ||
~~~~~~ | ||
|
||
This section documents some of the data fields that may be present. It is not | ||
extensive — different implementations, platforms etc. may include additional | ||
fields. | ||
|
||
Dictionaries represent sub-sections, and their names are separated with | ||
dots (``.``) in this documentation. | ||
|
||
``python.version`` | ||
++++++++++++++++++ | ||
|
||
:Type: ``str`` | ||
:Description: A string representation of the Python version. | ||
|
||
``python.version_parts`` | ||
++++++++++++++++++++++++ | ||
|
||
:Type: ``dict[str, int | str]`` | ||
:Description: A dictionary containing the different components of the Python | ||
version, as found in :py:data:`sys.version_info`. | ||
|
||
``python.executable`` | ||
+++++++++++++++++++++ | ||
|
||
:Type: ``str`` | ||
:Description: An absolute path pointing to an interpreter from the installation. | ||
:Availability: May be omitted in situations where it is unavailable, there isn't | ||
a reliable path that can be specified, etc. | ||
|
||
``python.stdlib`` | ||
+++++++++++++++++++++ | ||
|
||
:Type: ``str`` | ||
:Description: An absolute path pointing to the directory where the standard | ||
library is installed. | ||
:Availability: May be omitted in situations where it is unavailable, there isn't | ||
a reliable path that can be specified, etc. | ||
|
||
|
||
.. _introspection-install-details-location: | ||
|
||
Location | ||
~~~~~~~~ | ||
|
||
On standard CPython installations, when available, the details file will be | ||
installed in the same directory as the standard library, with the name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In some cases the standard library is split. Can we name a specific file it will be alongside? (e.g. the tests below expect it to be next to Also, on Windows, this implies that everyone should hard-code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, I will clarify it is the pure part of the standard library, which corresponds to the sysconfig
I don't see a great solution for this, given that The approach for Python launchers is not optimal, but it can be fixed later. Currently, I am just focusing on getting the file format down, and making the file available somewhere. Afterwards, we can look at the discoverability issue and consider for eg. having a global registry where these files could be additionally installed (eg. on Linux this could be the What do you think? |
||
``build-details.json``. | ||
|
||
Different implementations may place it in a different path, choose to provide | ||
the file via a different mechanism, or choose not to include it at all. | ||
|
||
.. note:: For Python implementation maintainers | ||
|
||
It would be good to try to align the install location between | ||
implementations, when possible, to help with compatibility. | ||
|
||
We just do not choose a installation path for everyone because different | ||
implementations may work differently, and it should be up to each project to | ||
decide what makes the most sense for them. | ||
|
||
|
||
.. _introspection-stdlib-clis: | ||
|
||
Standard library modules with a CLI | ||
----------------------------------- | ||
|
||
Some standard library modules include a CLI that exposes information, which can | ||
be helpful for introspection. | ||
|
||
- :mod:`sysconfig` (see :ref:`sysconfig-commandline`) | ||
- :mod:`site` (see :ref:`site-commandline`) | ||
|
||
|
||
.. _introspection-python-config-script: | ||
|
||
The ``python-config`` script | ||
---------------------------- | ||
|
||
.. TODO: Currently, we don't have any documentation covering python-config, but | ||
if/when we add some, we refer to it here, instead. | ||
|
||
.. availability:: POSIX | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Windows, probably the best equivalent is to recommend a PEP 514 addition to specify the full path to |
||
|
||
A ``python-config`` script may be available alongside the interpreter | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this script grow an option to reveal the location of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's reasonable, but I'd rather put it in the sysconfig CLI, which is less problematic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Except we need a way to locate the file without launching the runtime? Which is why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right 🤦 I just remembered that now when looking at GH-77620. |
||
executable. This script exposes information especially relevant when building C | ||
extensions. | ||
|
||
When using it via ``PATH``, you should be careful with your environment, and | ||
make sure that the first ``python-config`` entry does in fact belong to the | ||
correct interpreter. | ||
|
||
Additionally, the current implementation does not need to run the interpreter, | ||
so this script may be helpful in situtation where that is not possible, or | ||
undesirable. Though, please keep in mind that this is an implementation detail | ||
and no guarantees are made regarding this aspect of the implementation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import json | ||
import os | ||
import sys | ||
import sysconfig | ||
import string | ||
import unittest | ||
|
||
|
||
class FormatTestsBase: | ||
@property | ||
def contents(self): | ||
"""Install details file contents. Should be overriden by subclasses.""" | ||
raise NotImplementedError | ||
|
||
@property | ||
def data(self): | ||
"""Parsed install details file data, as a Python object.""" | ||
return json.loads(self.contents) | ||
|
||
def key(self, name): | ||
"""Helper to fetch subsection entries. | ||
|
||
It takes the entry name, allowing the usage of a dot to separate the | ||
different subsection names (eg. specifying 'a.b.c' as the key will | ||
return the value of self.data['a']['b']['c']). | ||
""" | ||
value = self.data | ||
for part in name.split('.'): | ||
value = value[part] | ||
return value | ||
|
||
def test_parse(self): | ||
self.data | ||
|
||
def test_top_level_container(self): | ||
self.assertIsInstance(self.data, dict) | ||
for key, value in self.data.items(): | ||
with self.subTest(section=key): | ||
self.assertIsInstance(value, dict) | ||
|
||
def test_python_version(self): | ||
allowed_characters = string.digits + string.ascii_letters + '.' | ||
value = self.key('python.version') | ||
|
||
self.assertLessEqual(set(value), set(allowed_characters)) | ||
self.assertTrue(sys.version.startswith(value)) | ||
|
||
def test_python_version_parts(self): | ||
value = self.key('python.version_parts') | ||
|
||
self.assertEqual(len(value), sys.version_info.n_fields) | ||
for part_name, part_value in value.items(): | ||
with self.subTest(part=part_name): | ||
self.assertEqual(part_value, getattr(sys.version_info, part_name)) | ||
|
||
def test_python_executable(self): | ||
"""Test the python.executable entry. | ||
|
||
The generic test wants the key to be missing. If your implementation | ||
provides a value for it, you should override this test. | ||
""" | ||
with self.assertRaises(KeyError): | ||
self.key('python.executable') | ||
|
||
def test_python_stdlib(self): | ||
"""Test the python.stdlib entry. | ||
|
||
The generic test wants the key to be missing. If your implementation | ||
provides a value for it, you should override this test. | ||
""" | ||
with self.assertRaises(KeyError): | ||
self.key('python.stdlib') | ||
|
||
|
||
needs_installed_python = unittest.skipIf( | ||
sysconfig.is_python_build(), | ||
'This test can only run in an installed Python', | ||
) | ||
|
||
|
||
@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX right now') | ||
class CPythonInstallDetailsFileTests(unittest.TestCase, FormatTestsBase): | ||
"""Test CPython's install details file implementation.""" | ||
|
||
@property | ||
def location(self): | ||
if sysconfig.is_python_build(): | ||
dirname = sysconfig.get_config_var('projectbase') | ||
else: | ||
dirname = sysconfig.get_path('stdlib') | ||
return os.path.join(dirname, 'build-details.json') | ||
|
||
@property | ||
def contents(self): | ||
with open(self.location, 'r') as f: | ||
return f.read() | ||
|
||
def setUp(self): | ||
if sys.platform in ('wasi', 'emscripten') and not os.path.isfile(self.location): | ||
self.skipTest(f'{sys.platform} build without a build-details.json file') | ||
|
||
@needs_installed_python | ||
def test_location(self): | ||
self.assertTrue(os.path.isfile(self.location)) | ||
|
||
# Override generic format tests with tests for our specific implemenation. | ||
|
||
@needs_installed_python | ||
def test_python_executable(self): | ||
value = self.key('python.executable') | ||
|
||
self.assertEqual(os.path.realpath(value), os.path.realpath(sys.executable)) | ||
|
||
@needs_installed_python | ||
def test_python_stdlib(self): | ||
value = self.key('python.stdlib') | ||
|
||
try: | ||
stdlib = os.path.dirname(sysconfig.__file__) | ||
except AttributeError as exc: | ||
self.skipTest(str(exc)) | ||
|
||
self.assertEqual(os.path.realpath(value), os.path.realpath(stdlib)) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
A ``build-details.json`` file containing information regarding the Python | ||
build/installation is now installed in the same directory as the standard | ||
library. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make these paths (optionally) relative from the directory containing the file? That way it can be static for most scenarios, instead of needing to be dynamically generated (and e.g. in the Windows Store package we can't dynamically generate it and also put it into the expected location, because it's read-only).
Alternatively, include a few variables like
{prefix}
that the reader can substitute themselves.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that makes sense for use-cases like yours.