Python pyproject.toml Practice Problems & Exercises
Practice: pyproject.toml
← Back to lessonEasy
Parse a pyproject.toml string and extract the standard [project] metadata. This is what pip, build tools, and package registries read to understand your project.
try:
import tomllib
except ImportError:
try:
import tomli as tomllib
except ImportError:
tomllib = None
def parse_project_metadata(toml_content):
if tomllib is None:
# Minimal fallback: extract name with simple parsing
name = None
for line in toml_content.splitlines():
if line.strip().startswith('name'):
_, _, val = line.partition('=')
name = val.strip().strip('"\'')
break
return {'name': name, 'version': None, 'description': None,
'requires_python': None, 'dependencies': [], 'authors': []}
data = tomllib.loads(toml_content)
project = data.get('project', {})
return {
'name': project.get('name'),
'version': project.get('version'),
'description': project.get('description'),
'requires_python': project.get('requires-python'),
'dependencies': project.get('dependencies', []),
'authors': project.get('authors', []),
}
sample = """
[project]
name = "mypackage"
version = "1.0.0"
description = "A sample package"
requires-python = ">=3.9"
dependencies = ["requests>=2.28"]
[[project.authors]]
name = "AI Engineer"
email = "[email protected]"
"""
result = parse_project_metadata(sample)
print(result['name'], result['version'])Solution
try:
import tomllib
except ImportError:
import tomli as tomllib
def parse_project_metadata(toml_content):
data = tomllib.loads(toml_content)
project = data.get('project', {})
return {
'name': project.get('name'),
'version': project.get('version'),
'description': project.get('description'),
'requires_python': project.get('requires-python'),
'dependencies': project.get('dependencies', []),
'authors': project.get('authors', []),
}
tomllib was added to the Python stdlib in 3.11. For older Pythons, install tomli (the backport). Notice requires-python uses a hyphen in TOML but most Python code uses requires_python with an underscore — the hyphen is the standard PEP 621 key name. The [[project.authors]] syntax creates an array of tables — each [[...]] block appends an entry to the list. This is TOML's way of expressing a list of objects.
def parse_project_metadata(toml_content):
"""Parse a pyproject.toml string and extract
standard [project] metadata.
Return a dict with keys (all from [project] table):
- 'name': str
- 'version': str or None
- 'description': str or None
- 'requires_python': str or None
- 'dependencies': list of str
- 'authors': list of dicts with 'name' and/or 'email'
Use the tomllib module (Python 3.11+) or fallback parsing.
"""
# TODO: implement
passExpected Output
{'name': 'mypackage', 'version': '1.0.0', 'description': 'A sample package', 'requires_python': '>=3.9', 'dependencies': ['requests>=2.28'], 'authors': [{'name': 'AI Engineer', 'email': '[email protected]'}]}Hints
Hint 1: Use tomllib.loads(toml_content) to parse the TOML string into a dict.
Hint 2: The [project] table maps to data["project"]. Access sub-keys with .get() for optional fields.
Write a validator for [project] table contents. Build tools like hatchling and flit run similar validation before accepting a pyproject.toml.
import re
def validate_project_table(project_dict):
errors = []
if 'name' not in project_dict or not project_dict['name']:
errors.append('name is required')
elif not isinstance(project_dict['name'], str):
errors.append('name must be a string')
if 'version' not in project_dict or not project_dict['version']:
errors.append('version is required')
elif not isinstance(project_dict['version'], str):
errors.append('version must be a string')
if 'requires-python' in project_dict:
rp = project_dict['requires-python']
if not re.match(r'^[><=!~]', rp):
errors.append('requires-python must start with a version operator')
if 'dependencies' in project_dict:
if not isinstance(project_dict['dependencies'], list):
errors.append('dependencies must be a list')
return {'valid': len(errors) == 0, 'errors': errors}
print(validate_project_table({}))
print(validate_project_table({'name': 'mypkg', 'version': '1.0.0'}))
print(validate_project_table({'name': 'mypkg', 'version': '1.0.0', 'requires-python': '>=3.9'}))Solution
import re
def validate_project_table(project_dict):
errors = []
if 'name' not in project_dict or not project_dict['name']:
errors.append('name is required')
elif not isinstance(project_dict['name'], str):
errors.append('name must be a string')
if 'version' not in project_dict or not project_dict['version']:
errors.append('version is required')
elif not isinstance(project_dict['version'], str):
errors.append('version must be a string')
if 'requires-python' in project_dict:
rp = project_dict['requires-python']
if not re.match(r'^[><=!~]', rp):
errors.append('requires-python must start with a version operator')
if 'dependencies' in project_dict:
if not isinstance(project_dict['dependencies'], list):
errors.append('dependencies must be a list')
return {'valid': len(errors) == 0, 'errors': errors}
PEP 621 makes name and version the only mandatory fields, though build backends may require additional fields. The name field is normalized: my-package, my_package, and mypackage are all the same package (normalization replaces hyphens and underscores with - and lowercases). PyPI enforces this normalization — you cannot register my-package and my_package as different packages. The validate-pyproject CLI tool provides full PEP 621 validation using JSON Schema.
def validate_project_table(project_dict):
"""Validate that a project metadata dict has all
required fields and that they have correct types.
Required: name (str), version (str)
Optional but validated if present:
- requires-python: must match pattern like '>=3.9'
- dependencies: must be a list
Return a dict:
- 'valid': bool
- 'errors': list of error strings
"""
# TODO: implement
passExpected Output
{'valid': False, 'errors': ['name is required', 'version is required']}Hints
Hint 1: Check for key existence with "key in project_dict". Check type with isinstance().
Hint 2: For requires-python, use a regex to validate the pattern starts with a valid operator.
Parse console script entry points from [project.scripts]. When pip installs a package with scripts defined, it creates wrapper executables in venv/bin/ that call the specified Python functions.
def extract_entry_points(project_dict):
scripts = project_dict.get('scripts', {})
result = []
for name, target in scripts.items():
if ':' in target:
module, _, function = target.partition(':')
result.append({
'name': name,
'module': module.strip(),
'function': function.strip(),
})
return result
project = {
'name': 'mypackage',
'scripts': {
'mycli': 'mypackage.cli:main',
'myserver': 'mypackage.server:run',
}
}
print(extract_entry_points(project))Solution
def extract_entry_points(project_dict):
scripts = project_dict.get('scripts', {})
result = []
for name, target in scripts.items():
if ':' in target:
module, _, function = target.partition(':')
result.append({
'name': name,
'module': module.strip(),
'function': function.strip(),
})
return result
Entry points are how pip creates executable CLI commands. When pip installs a package with [project.scripts], it generates a small Python wrapper script in venv/bin/ that imports the module and calls the function. This is how tools like black, pytest, flask, and uvicorn become available as commands after pip install. The [project.entry-points] table is the more general form — it allows plugins to register themselves for any named group (e.g., pytest11 for pytest plugins, babel.extractors for Babel).
def extract_entry_points(project_dict):
"""Extract console script entry points from a project dict.
project_dict may contain:
[project.scripts]
mycli = "mypackage.cli:main"
Return a list of dicts:
- 'name': the CLI command name
- 'module': the Python module path
- 'function': the function name
Example: 'mycli = "mypackage.cli:main"' ->
{'name': 'mycli', 'module': 'mypackage.cli', 'function': 'main'}
"""
# TODO: implement
passExpected Output
[{'name': 'mycli', 'module': 'mypackage.cli', 'function': 'main'}]Hints
Hint 1: Scripts are in project_dict.get("scripts", {}). Each key is the command name, value is "module:function".
Hint 2: Split the value on ":" to get module and function.
Write a backend identifier for pyproject.toml files. Different backends have different feature sets and configuration tables — knowing which backend a project uses is the first step in any automated build system interaction.
def identify_build_backend(toml_dict):
build_system = toml_dict.get('build-system', {})
backend_path = build_system.get('build-backend', '')
requires = build_system.get('requires', [])
mapping = {
'hatchling': 'hatchling',
'setuptools': 'setuptools',
'flit_core': 'flit',
'poetry': 'poetry',
'maturin': 'maturin',
'pdm': 'pdm-backend',
}
backend = 'unknown'
for key, name in mapping.items():
if key in backend_path:
backend = name
break
return {
'backend': backend,
'backend_path': backend_path,
'requires': requires,
}
data = {
'build-system': {
'requires': ['hatchling'],
'build-backend': 'hatchling.build',
}
}
print(identify_build_backend(data))Solution
def identify_build_backend(toml_dict):
build_system = toml_dict.get('build-system', {})
backend_path = build_system.get('build-backend', '')
requires = build_system.get('requires', [])
mapping = {
'hatchling': 'hatchling', 'setuptools': 'setuptools',
'flit_core': 'flit', 'poetry': 'poetry',
'maturin': 'maturin', 'pdm': 'pdm-backend',
}
backend = 'unknown'
for key, name in mapping.items():
if key in backend_path:
backend = name
break
return {'backend': backend, 'backend_path': backend_path, 'requires': requires}
The [build-system] table was standardised in PEP 518 (2016) to declare build dependencies separately from runtime dependencies. Before PEP 518, pip had to guess that a project needed setuptools and install it before anything else could happen. Now pip reads build-system.requires, installs exactly those packages into an isolated build environment, then calls build-system.build-backend to produce the wheel. This isolation means build tools do not pollute your runtime environment.
def identify_build_backend(toml_dict):
"""Identify the build backend from a parsed pyproject.toml.
Check [build-system].build-backend.
Return one of: 'hatchling', 'setuptools', 'flit', 'poetry',
'maturin', 'pdm-backend', or 'unknown'.
Also return the requires list from [build-system].
Return a dict:
- 'backend': str
- 'backend_path': str (the full build-backend value)
- 'requires': list of str
"""
# TODO: implement
passExpected Output
{'backend': 'hatchling', 'backend_path': 'hatchling.build', 'requires': ['hatchling']}Hints
Hint 1: Access toml_dict.get("build-system", {}).get("build-backend", "").
Hint 2: Map the backend path to a short name: "hatchling.build" -> "hatchling", "setuptools.build_meta" -> "setuptools".
Medium
Resolve the full dependency set for a package installed with extras — pip install mypackage[web,aws]. The result is the base dependencies plus all deps from the requested extra groups.
import re
def extract_pkg_name(dep):
return re.split(r'[>=<!~\[;\s]', dep.strip())[0].lower()
def resolve_extras(project_dict, requested_extras):
all_deps = list(project_dict.get('dependencies', []))
optional = project_dict.get('optional-dependencies', {})
for extra in requested_extras:
if extra in optional:
all_deps.extend(optional[extra])
# Deduplicate by package name, keeping last occurrence
seen = {}
for dep in all_deps:
name = extract_pkg_name(dep)
seen[name] = dep
return sorted(seen.values(), key=extract_pkg_name)
project = {
'dependencies': ['requests>=2.28'],
'optional-dependencies': {
'web': ['uvicorn>=0.23', 'fastapi>=0.100'],
'aws': ['boto3>=1.28'],
'dev': ['pytest>=7.0', 'mypy>=1.0'],
}
}
print(resolve_extras(project, ['web', 'aws']))Solution
import re
def extract_pkg_name(dep):
return re.split(r'[>=<!~\[;\s]', dep.strip())[0].lower()
def resolve_extras(project_dict, requested_extras):
all_deps = list(project_dict.get('dependencies', []))
optional = project_dict.get('optional-dependencies', {})
for extra in requested_extras:
if extra in optional:
all_deps.extend(optional[extra])
seen = {}
for dep in all_deps:
seen[extract_pkg_name(dep)] = dep
return sorted(seen.values(), key=extract_pkg_name)
Optional dependencies are pip's way of expressing conditional feature sets. pip install requests[security] installs the cryptography and pyOpenSSL packages that are not needed for basic HTTP but required for certificate verification. pip install fastapi[all] installs all optional server/testing extras. This lets package authors keep a minimal install fast while offering a batteries-included option. The [dev] extra convention is used with pip install -e ".[dev]" to install development tools without making them runtime requirements.
def resolve_extras(project_dict, requested_extras):
"""Resolve which dependencies to install given
a set of requested extras.
project_dict has:
- 'dependencies': base deps list
- 'optional-dependencies': dict of extra -> deps list
requested_extras: list of extra names
Return a sorted, deduplicated list of all deps
(base + requested extras combined).
"""
# TODO: implement
passExpected Output
['boto3>=1.28', 'requests>=2.28', 'uvicorn>=0.23']Hints
Hint 1: Start with the base dependencies list. For each requested extra, extend with its deps.
Hint 2: Normalise package names (lowercase) before deduplication to handle case differences.
Generate a complete pyproject.toml string from structured metadata. This is what hatch new, poetry new, and project scaffolding tools do when you run them.
def generate_pyproject_toml(metadata):
deps = metadata.get('dependencies', [])
dev_deps = metadata.get('dev_dependencies', [])
def fmt_list(items):
if not items:
return '[]'
lines = ['[']
for item in items:
lines.append(' "' + item + '",')
lines.append(']')
return '\n'.join(lines)
parts = [
'[build-system]',
'requires = ["hatchling"]',
'build-backend = "hatchling.build"',
'',
'[project]',
'name = "' + metadata['name'] + '"',
'version = "' + metadata['version'] + '"',
'description = "' + metadata.get('description', '') + '"',
'requires-python = "' + metadata.get('requires_python', '>=3.9') + '"',
'dependencies = ' + fmt_list(deps),
'',
'[[project.authors]]',
'name = "' + metadata.get('author_name', '') + '"',
'email = "' + metadata.get('author_email', '') + '"',
]
if dev_deps:
parts += [
'',
'[project.optional-dependencies]',
'dev = ' + fmt_list(dev_deps),
]
return '\n'.join(parts)
meta = {
'name': 'mypackage',
'version': '0.1.0',
'description': 'My awesome package',
'requires_python': '>=3.10',
'author_name': 'AI Engineer',
'author_email': '[email protected]',
'dependencies': ['requests>=2.28', 'click>=8.0'],
'dev_dependencies': ['pytest>=7.0', 'mypy>=1.0'],
}
print(generate_pyproject_toml(meta))Solution
def generate_pyproject_toml(metadata):
deps = metadata.get('dependencies', [])
dev_deps = metadata.get('dev_dependencies', [])
def fmt_list(items):
if not items:
return '[]'
return '[\n' + '\n'.join(' "' + i + '",' for i in items) + '\n]'
parts = [
'[build-system]', 'requires = ["hatchling"]', 'build-backend = "hatchling.build"', '',
'[project]', 'name = "' + metadata['name'] + '"', 'version = "' + metadata['version'] + '"',
'description = "' + metadata.get('description', '') + '"',
'requires-python = "' + metadata.get('requires_python', '>=3.9') + '"',
'dependencies = ' + fmt_list(deps), '',
'[[project.authors]]',
'name = "' + metadata.get('author_name', '') + '"',
'email = "' + metadata.get('author_email', '') + '"',
]
if dev_deps:
parts += ['', '[project.optional-dependencies]', 'dev = ' + fmt_list(dev_deps)]
return '\n'.join(parts)
TOML generation requires careful attention to quoting and nesting. Project scaffolding tools like cookiecutter, copier, and hatch new use template engines (Jinja2) rather than string concatenation to generate these files safely. The section order in pyproject.toml is conventionally: [build-system] first, then [project], then optional-dependencies, then tool configs like [tool.pytest], [tool.mypy], [tool.ruff]. Following this convention makes the file readable to humans and consistent across projects.
def generate_pyproject_toml(metadata):
"""Generate a pyproject.toml string from a metadata dict.
metadata keys:
- name: str
- version: str
- description: str
- requires_python: str (e.g. '>=3.9')
- author_name: str
- author_email: str
- dependencies: list of str
- dev_dependencies: list of str (optional extras)
Use hatchling as the build backend.
Return a properly formatted TOML string.
"""
# TODO: implement
passExpected Output
[build-system]
requires = ["hatchling"]
...Hints
Hint 1: Build the string section by section. For lists, format each item as " \"dep\"," on its own line.
Hint 2: The [build-system] section must come before [project] by convention.
Validate that a project follows the src layout convention. The src layout prevents accidental imports of the development version of a package — when running tests, Python finds the installed version in site-packages, not the local source tree.
import os
def validate_src_layout(project_root):
errors = []
warnings = []
package_name = None
if not os.path.exists(os.path.join(project_root, 'pyproject.toml')):
errors.append('pyproject.toml not found at project root')
src_dir = os.path.join(project_root, 'src')
if not os.path.isdir(src_dir):
errors.append('src/ directory not found')
else:
packages = [
d for d in os.listdir(src_dir)
if os.path.isdir(os.path.join(src_dir, d))
and not d.startswith('.')
and not d.endswith('.egg-info')
]
if not packages:
errors.append('No Python package found inside src/')
else:
package_name = packages[0]
init_path = os.path.join(src_dir, package_name, '__init__.py')
if not os.path.exists(init_path):
warnings.append('src/' + package_name + '/__init__.py not found')
tests_dir = os.path.join(project_root, 'tests')
if not os.path.isdir(tests_dir):
warnings.append('tests/ directory not found')
return {
'valid': len(errors) == 0,
'errors': errors,
'warnings': warnings,
'package_name': package_name,
}
import tempfile
with tempfile.TemporaryDirectory() as tmp:
os.makedirs(os.path.join(tmp, 'src', 'mypackage'))
open(os.path.join(tmp, 'pyproject.toml'), 'w').close()
open(os.path.join(tmp, 'src', 'mypackage', '__init__.py'), 'w').close()
os.makedirs(os.path.join(tmp, 'tests'))
print(validate_src_layout(tmp))Solution
import os
def validate_src_layout(project_root):
errors, warnings = [], []
package_name = None
if not os.path.exists(os.path.join(project_root, 'pyproject.toml')):
errors.append('pyproject.toml not found at project root')
src_dir = os.path.join(project_root, 'src')
if not os.path.isdir(src_dir):
errors.append('src/ directory not found')
else:
packages = [
d for d in os.listdir(src_dir)
if os.path.isdir(os.path.join(src_dir, d))
and not d.startswith('.') and not d.endswith('.egg-info')
]
if not packages:
errors.append('No Python package found inside src/')
else:
package_name = packages[0]
if not os.path.exists(os.path.join(src_dir, package_name, '__init__.py')):
warnings.append('src/' + package_name + '/__init__.py not found')
if not os.path.isdir(os.path.join(project_root, 'tests')):
warnings.append('tests/ directory not found')
return {'valid': len(errors) == 0, 'errors': errors,
'warnings': warnings, 'package_name': package_name}
The src layout solves the "accidental local import" problem. With a flat layout (package at project root), running pytest from the project root puts the project root on sys.path, so import mypackage finds the local source instead of the installed package. This means you are always testing the source tree, never the built artifact. With src layout, the source is not on sys.path by default — you must install the package (pip install -e .) to import it, which means you always test what's actually installed. This is the recommendation for any package that will be published to PyPI.
import os
def validate_src_layout(project_root):
"""Validate that a project follows the src layout convention.
Expected structure:
project_root/
pyproject.toml
src/
package_name/
__init__.py
tests/
Return a dict:
- 'valid': bool
- 'errors': list of strings
- 'warnings': list of strings
- 'package_name': str or None (detected from src/)
"""
# TODO: implement
passExpected Output
{'valid': True, 'errors': [], 'warnings': [], 'package_name': 'mypackage'}Hints
Hint 1: Check os.path.exists for pyproject.toml, src/, and tests/. List src/ contents to find the package dir.
Hint 2: A warning (not error) if tests/ is missing. An error if pyproject.toml is missing.
Extract and summarize all [tool.*] configurations from a pyproject.toml. Modern Python projects consolidate all tool configuration into pyproject.toml — no more setup.cfg, .mypy.ini, .flake8, or pytest.ini cluttering the project root.
def extract_tool_configs(toml_dict):
tool_section = toml_dict.get('tool', {})
known = {
'pytest': 'Test runner configuration',
'mypy': 'Static type checker',
'ruff': 'Fast linter and formatter',
'black': 'Code formatter',
'isort': 'Import sorter',
'coverage': 'Test coverage tool',
}
tools = sorted(tool_section.keys())
return {
'tools': tools,
'configs': {name: tool_section[name] for name in tools},
'summary': {name: known.get(name, 'Custom tool configuration') for name in tools},
}
data = {
'tool': {
'pytest': {'ini_options': {'testpaths': ['tests'], 'verbose': True}},
'mypy': {'strict': True, 'ignore_missing_imports': True},
'ruff': {'line-length': 88, 'select': ['E', 'F', 'I']},
}
}
result = extract_tool_configs(data)
print("tools:", result['tools'])
print("summaries:", result['summary'])Solution
def extract_tool_configs(toml_dict):
tool_section = toml_dict.get('tool', {})
known = {
'pytest': 'Test runner configuration', 'mypy': 'Static type checker',
'ruff': 'Fast linter and formatter', 'black': 'Code formatter',
'isort': 'Import sorter', 'coverage': 'Test coverage tool',
}
tools = sorted(tool_section.keys())
return {
'tools': tools,
'configs': {name: tool_section[name] for name in tools},
'summary': {name: known.get(name, 'Custom tool configuration') for name in tools},
}
Consolidating tool config into pyproject.toml is one of the biggest DX improvements in modern Python. A mature project previously had: setup.py, setup.cfg, MANIFEST.in, tox.ini, pytest.ini, .mypy.ini, .flake8, .isort.cfg, .coveragerc — nine files for one project. Now all of that lives in pyproject.toml under [tool.X] tables. The only tool that cannot use pyproject.toml yet is tox (as of 2024), though support is planned. The ruff linter has effectively replaced flake8 + isort + pyupgrade with a single [tool.ruff] config block.
def extract_tool_configs(toml_dict):
"""Extract all [tool.X] configurations from a
parsed pyproject.toml dict.
Return a dict:
- 'tools': list of tool names found
- 'configs': dict mapping tool_name -> config_dict
- 'summary': dict mapping tool_name -> one-line description
For summary, use these known descriptions:
'pytest': 'Test runner configuration',
'mypy': 'Static type checker',
'ruff': 'Fast linter and formatter',
'black': 'Code formatter',
'isort': 'Import sorter',
'coverage': 'Test coverage tool',
For unknown tools, use 'Custom tool configuration'.
"""
# TODO: implement
passExpected Output
{'tools': ['mypy', 'pytest', 'ruff'], 'configs': {...}, 'summary': {...}}Hints
Hint 1: Access toml_dict.get("tool", {}) to get all tool configurations.
Hint 2: Sort the tool names for consistent output.
Hard
Implement dynamic versioning from git tags. This is what hatch-vcs, setuptools-scm, and versioneer do — derive the package version from git state rather than hard-coding it in pyproject.toml.
The advantage: the version is always in sync with your release tag, and development builds get unique version identifiers.
import subprocess
import re
def get_version_from_git(repo_path='.'):
try:
output = subprocess.check_output(
['git', 'describe', '--tags', '--long', '--match', 'v[0-9]*'],
cwd=repo_path,
stderr=subprocess.DEVNULL,
text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return '0.0.0.dev0'
# Format: v1.2.3-5-gabc1234
match = re.match(r'^v?(\d+\.\d+\.\d+)-(\d+)-g([a-f0-9]+)$', output)
if not match:
return '0.0.0.dev0'
base_version, distance, commit_hash = match.group(1), match.group(2), match.group(3)
if distance == '0':
return base_version
return base_version + '.dev' + distance + '+g' + commit_hash
version = get_version_from_git()
print("Version:", version)Solution
import subprocess
import re
def get_version_from_git(repo_path='.'):
try:
output = subprocess.check_output(
['git', 'describe', '--tags', '--long', '--match', 'v[0-9]*'],
cwd=repo_path, stderr=subprocess.DEVNULL, text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return '0.0.0.dev0'
match = re.match(r'^v?(\d+\.\d+\.\d+)-(\d+)-g([a-f0-9]+)$', output)
if not match:
return '0.0.0.dev0'
base, distance, ghash = match.group(1), match.group(2), match.group(3)
return base if distance == '0' else base + '.dev' + distance + '+g' + ghash
Dynamic versioning solves the "version bump commit" problem. Without it, every release requires: bump version in pyproject.toml, commit, tag, release — creating a chicken-and-egg situation where the release commit changes files. With dynamic versioning, you just create the tag and the version is automatically correct. The .devN suffix marks pre-release builds as lower than the release version in PEP 440 ordering: 1.2.3.dev5 less than 1.2.3. The +g local version identifier is stripped by PyPI (it does not accept local versions), so CI can build installable wheels from any commit.
import subprocess
import re
def get_version_from_git(repo_path='.'):
"""Derive a PEP 440 version string from git tags.
Logic:
1. Get the latest git tag matching vX.Y.Z.
2. Check how many commits since that tag.
3. If 0 commits since tag: return 'X.Y.Z'
4. If N > 0 commits: return 'X.Y.Z.devN+gHASH'
5. If no tags found: return '0.0.0.dev0'
Use subprocess to run git commands.
Return a version string.
"""
# TODO: implement
passExpected Output
'1.2.3' or '1.2.3.dev5+gabc1234'Hints
Hint 1: Run: git describe --tags --long --match "v[0-9]*" to get "v1.2.3-5-gabc1234" format.
Hint 2: Parse the output: split on "-" to get tag, commit count, and hash. Strip the "v" prefix from the tag.
Simulate the build backend interface from PEP 517. Understand what happens when python -m build or pip wheel is called — the series of steps that transform source code into an installable artifact.
import os
import re
def normalize_wheel_name(name):
return re.sub(r'[-_.]+', '_', name).lower()
def read_name_version(project_root):
cfg_path = os.path.join(project_root, 'pyproject.toml')
name, version = 'unknown', '0.0.0'
if not os.path.exists(cfg_path):
return name, version
with open(cfg_path) as f:
content = f.read()
for line in content.splitlines():
if re.match(r'\s*name\s*=', line) and name == 'unknown':
name = line.partition('=')[2].strip().strip('"\'')
if re.match(r'\s*version\s*=', line) and version == '0.0.0':
version = line.partition('=')[2].strip().strip('"\'')
return name, version
def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
name, version = read_name_version(project_root)
wheel_name = normalize_wheel_name(name)
if artifact_type == 'wheel':
filename = wheel_name + '-' + version + '-py3-none-any.whl'
build_steps = [
'Read pyproject.toml metadata',
'Validate project structure',
'Collect source files from src/' + name,
'Write METADATA to ' + wheel_name + '-' + version + '.dist-info/METADATA',
'Write WHEEL tag file',
'Write RECORD checksums',
'Zip all files into ' + filename,
]
metadata_files = [
wheel_name + '-' + version + '.dist-info/METADATA',
wheel_name + '-' + version + '.dist-info/WHEEL',
wheel_name + '-' + version + '.dist-info/RECORD',
]
else:
filename = name + '-' + version + '.tar.gz'
build_steps = [
'Read pyproject.toml metadata',
'Collect all non-ignored source files',
'Generate PKG-INFO from project metadata',
'Create tar.gz archive as ' + filename,
]
metadata_files = ['PKG-INFO', 'pyproject.toml']
return {
'artifact_type': artifact_type,
'filename': filename,
'metadata_files': metadata_files,
'build_steps': build_steps,
}
import tempfile
with tempfile.TemporaryDirectory() as tmp:
with open(os.path.join(tmp, 'pyproject.toml'), 'w') as f:
f.write('[project]\nname = "my-package"\nversion = "1.0.0"\n')
result = simulate_build_backend(tmp, tmp, 'wheel')
print(result['filename'])
print(result['build_steps'][0])Solution
import os, re
def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
def read_name_version(root):
cfg = os.path.join(root, 'pyproject.toml')
name, version = 'unknown', '0.0.0'
if os.path.exists(cfg):
with open(cfg) as f:
for line in f:
if re.match(r'\s*name\s*=', line) and name == 'unknown':
name = line.partition('=')[2].strip().strip('"\'')
if re.match(r'\s*version\s*=', line) and version == '0.0.0':
version = line.partition('=')[2].strip().strip('"\'')
return name, version
name, version = read_name_version(project_root)
wn = re.sub(r'[-_.]+', '_', name).lower()
if artifact_type == 'wheel':
fname = wn + '-' + version + '-py3-none-any.whl'
steps = ['Read metadata', 'Collect source', 'Write dist-info', 'Zip wheel']
mfiles = [wn + '-' + version + '.dist-info/' + f for f in ['METADATA', 'WHEEL', 'RECORD']]
else:
fname = name + '-' + version + '.tar.gz'
steps = ['Read metadata', 'Collect sources', 'Generate PKG-INFO', 'Create tar.gz']
mfiles = ['PKG-INFO', 'pyproject.toml']
return {'artifact_type': artifact_type, 'filename': fname,
'metadata_files': mfiles, 'build_steps': steps}
PEP 517 defined the build backend protocol as a set of Python functions that pip calls via subprocess in an isolated environment: build_wheel(wheel_directory, config_settings) and build_sdist(sdist_directory, config_settings). This replaced the old python setup.py bdist_wheel approach which required importing setup.py (and all its side effects) in the build environment. The wheel filename format encodes compatibility: py3-none-any means pure Python 3, no ABI dependency, any platform. A C extension wheel might be cp311-cp311-manylinux_2_17_x86_64.
import os
def simulate_build_backend(project_root, output_dir, artifact_type='wheel'):
"""Simulate what a PEP 517 build backend does.
For artifact_type='wheel':
- Read pyproject.toml to get name and version
- Create a minimal wheel filename: name-version-py3-none-any.whl
- Return a dict describing what would be built
For artifact_type='sdist':
- Create filename: name-version.tar.gz
Return a dict:
- 'artifact_type': str
- 'filename': str
- 'metadata_files': list of files that would be included in the wheel
- 'build_steps': list of step description strings
"""
# TODO: implement
passExpected Output
{'artifact_type': 'wheel', 'filename': 'mypackage-1.0.0-py3-none-any.whl', ...}Hints
Hint 1: Parse pyproject.toml to get name and version. Normalize name: replace hyphens with underscores for wheel filename.
Hint 2: Wheel filename format: {name}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl
Generate a comprehensive dependency conflict report. This is what pip check does — it validates that the installed packages satisfy each other's requirements and reports any inconsistencies.
import re
def parse_dep(req_str):
req_str = req_str.strip()
parts = re.split(r'(>=|<=|==|!=|~=|>|<)', req_str, maxsplit=1)
name = parts[0].strip().lower()
specifier = None
if len(parts) > 1:
specifier = parts[1] + parts[2].strip().split(',')[0]
return name, specifier
def ver_tuple(v):
try:
return tuple(int(x) for x in v.split('.'))
except ValueError:
return (0,)
def version_satisfies(version_str, specifier_str):
ops = {'==': lambda a, b: a == b, '!=': lambda a, b: a != b,
'>=': lambda a, b: a >= b, '<=': lambda a, b: a <= b,
'>': lambda a, b: a > b, '<': lambda a, b: a < b}
for op in ('==', '!=', '>=', '<=', '>', '<'):
if specifier_str.startswith(op):
spec_v = specifier_str[len(op):]
v1 = ver_tuple(version_str)
v2 = ver_tuple(spec_v)
maxlen = max(len(v1), len(v2))
v1 += (0,) * (maxlen - len(v1))
v2 += (0,) * (maxlen - len(v2))
return ops[op](v1, v2)
return True
def generate_conflict_report(installed, project_deps):
installed_lower = {k.lower(): v for k, v in installed.items()}
satisfied, conflicts, missing = [], [], []
for req in project_deps:
name, specifier = parse_dep(req)
if name not in installed_lower:
missing.append(req)
continue
inst_ver = installed_lower[name]
if specifier and not version_satisfies(inst_ver, specifier):
conflicts.append({
'req': req,
'installed_version': inst_ver,
'reason': 'installed ' + inst_ver + ' does not satisfy ' + specifier,
})
else:
satisfied.append(req)
return {
'satisfied': satisfied,
'conflicts': conflicts,
'missing': missing,
'all_ok': len(conflicts) == 0 and len(missing) == 0,
}
installed = {'requests': '2.28.0', 'flask': '2.0.0', 'click': '8.1.0'}
deps = ['requests>=2.28', 'flask>=2.3.0', 'numpy>=1.24', 'click==8.1.0']
report = generate_conflict_report(installed, deps)
print("satisfied:", report['satisfied'])
print("conflicts:", report['conflicts'])
print("missing:", report['missing'])
print("all_ok:", report['all_ok'])Solution
import re
def generate_conflict_report(installed, project_deps):
def parse_dep(r):
parts = re.split(r'(>=|<=|==|!=|~=|>|<)', r.strip(), maxsplit=1)
name = parts[0].strip().lower()
spec = (parts[1] + parts[2].strip().split(',')[0]) if len(parts) > 1 else None
return name, spec
def ver_t(v):
try:
return tuple(int(x) for x in v.split('.'))
except ValueError:
return (0,)
def satisfies(version, spec):
ops = {'==': lambda a,b: a==b, '!=': lambda a,b: a!=b,
'>=': lambda a,b: a>=b, '<=': lambda a,b: a<=b,
'>': lambda a,b: a>b, '<': lambda a,b: a<b}
for op in ('==','!=','>=','<=','>','<'):
if spec.startswith(op):
v1, v2 = ver_t(version), ver_t(spec[len(op):])
n = max(len(v1), len(v2))
return ops[op](v1+(0,)*(n-len(v1)), v2+(0,)*(n-len(v2)))
return True
inst = {k.lower(): v for k, v in installed.items()}
satisfied, conflicts, missing = [], [], []
for req in project_deps:
name, spec = parse_dep(req)
if name not in inst:
missing.append(req)
elif spec and not satisfies(inst[name], spec):
conflicts.append({'req': req, 'installed_version': inst[name],
'reason': inst[name] + ' does not satisfy ' + spec})
else:
satisfied.append(req)
return {'satisfied': satisfied, 'conflicts': conflicts,
'missing': missing, 'all_ok': not conflicts and not missing}
pip check runs this exact logic across all installed packages — it reads every .dist-info/METADATA file, extracts Requires-Dist entries, and verifies that each installed package's dependencies are satisfied by what is actually installed. Conflicts are most common after manual pip install commands that upgrade one package without upgrading its dependents. The proper fix is always pip install package1 package2 in a single command — pip's resolver can then find a consistent set, whereas sequential installs may leave the environment in an inconsistent state.
def generate_conflict_report(installed, project_deps):
"""Generate a conflict analysis report.
installed: dict of package_name -> installed_version (strings)
project_deps: list of requirement strings (name+specifier)
For each project dependency, check if the installed version
satisfies the specifier. Report:
- satisfied requirements
- unsatisfied requirements (conflicts)
- missing packages
Return a dict:
- 'satisfied': list of req strings
- 'conflicts': list of dicts {req, installed_version, reason}
- 'missing': list of req strings
- 'all_ok': bool
"""
# TODO: implement
passExpected Output
{'satisfied': [...], 'conflicts': [...], 'missing': [...], 'all_ok': False}Hints
Hint 1: Reuse or inline the version_satisfies logic from Problem 2. Parse each dep to get name and specifiers.
Hint 2: A package is missing if its name is not in installed. It conflicts if installed but version fails specifiers.
