python
exercises
exercises.py🐍python
"""
Python Packaging - Exercises
Practice creating packages, configuration files, and project structure
"""
# ============================================================
# EXERCISE 1: Parse pyproject.toml
# ============================================================
"""
Create a function that parses a pyproject.toml file and extracts
key information like name, version, dependencies, etc.
Note: In real projects, use 'tomllib' (Python 3.11+) or 'tomli' package.
For this exercise, we'll use a simplified parser.
"""
def parse_pyproject(content: str) -> dict | None:
"""
Parse pyproject.toml content and extract package metadata.
Args:
content: String content of pyproject.toml
Returns:
Dictionary with keys: name, version, description,
python_requires, dependencies, dev_dependencies
Example:
>>> content = '''
... [project]
... name = "my-package"
... version = "0.1.0"
... '''
>>> result = parse_pyproject(content)
>>> result['name']
'my-package'
"""
# YOUR CODE HERE
# Hint: Use regex or simple string parsing
# This is a simplified version - real TOML parsing is more complex
pass
# Test data
test_pyproject = '''
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "example-package"
version = "1.2.3"
description = "An example package"
requires-python = ">=3.8"
dependencies = [
"requests>=2.25.0",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black",
]
'''
# print(parse_pyproject(test_pyproject))
# ============================================================
# EXERCISE 2: Version Comparator
# ============================================================
"""
Create a SemanticVersion class that can compare versions
according to semantic versioning rules.
"""
class SemanticVersion:
"""
Semantic version with comparison support.
Example:
>>> v1 = SemanticVersion("1.2.3")
>>> v2 = SemanticVersion("1.2.4")
>>> v1 < v2
True
>>> SemanticVersion("2.0.0") > SemanticVersion("1.9.9")
True
"""
def __init__(self, version_string: str):
"""Parse version string like '1.2.3' or '1.2.3-beta.1'."""
# YOUR CODE HERE
pass
def __eq__(self, other: 'SemanticVersion') -> bool: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def __lt__(self, other: 'SemanticVersion') -> bool: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def __le__(self, other: 'SemanticVersion') -> bool: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def __gt__(self, other: 'SemanticVersion') -> bool: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def __ge__(self, other: 'SemanticVersion') -> bool: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def __str__(self) -> str: # type: ignore[empty-body]
# YOUR CODE HERE
pass
def is_compatible(self, other: 'SemanticVersion') -> bool:
"""
Check if versions are compatible (same major version).
Breaking changes only in major version bumps.
"""
# YOUR CODE HERE
pass
# Test
# v1 = SemanticVersion("1.2.3")
# v2 = SemanticVersion("1.2.4")
# print(f"{v1} < {v2}: {v1 < v2}")
# print(f"{v1} compatible with {v2}: {v1.is_compatible(v2)}")
# ============================================================
# EXERCISE 3: Dependency Resolver
# ============================================================
"""
Create a simple dependency version checker that determines
if a package version satisfies a requirement specifier.
"""
import re
def version_satisfies(version: str, specifier: str) -> bool:
"""
Check if a version satisfies a requirement specifier.
Supported operators: ==, !=, <, <=, >, >=
Examples:
>>> version_satisfies("1.2.3", ">=1.0.0")
True
>>> version_satisfies("1.2.3", "<2.0.0")
True
>>> version_satisfies("1.2.3", ">=1.0.0,<2.0.0")
True
>>> version_satisfies("2.0.0", ">=1.0.0,<2.0.0")
False
"""
# YOUR CODE HERE
pass
# Test cases
# print(version_satisfies("1.2.3", ">=1.0.0")) # True
# print(version_satisfies("1.2.3", "==1.2.3")) # True
# print(version_satisfies("1.2.3", ">=1.0.0,<2.0.0")) # True
# print(version_satisfies("0.9.0", ">=1.0.0")) # False
# ============================================================
# EXERCISE 4: Requirements Parser
# ============================================================
"""
Parse a requirements.txt file and extract package information.
Handle comments, blank lines, and various requirement formats.
"""
from typing import List, Dict, Optional
def parse_requirements(content: str) -> List[Dict]:
"""
Parse requirements.txt content.
Returns list of dicts with keys: name, version_spec, extras, markers
Example:
>>> content = '''
... requests>=2.25.0
... click[cli]>=8.0.0
... # This is a comment
... numpy==1.24.0 ; python_version >= "3.8"
... '''
>>> reqs = parse_requirements(content)
>>> reqs[0]['name']
'requests'
"""
# YOUR CODE HERE
pass
# Test
test_requirements = """
# Core dependencies
requests>=2.25.0
click>=8.0.0
# Optional features
pandas[excel]>=1.5.0
# Development
pytest>=7.0 ; python_version >= "3.8"
# Empty lines are ignored
# Pinned version
numpy==1.24.0
"""
# print(parse_requirements(test_requirements))
# ============================================================
# EXERCISE 5: Package Name Validator
# ============================================================
"""
Validate Python package names according to PEP 503/508 rules.
"""
def validate_package_name(name: str) -> tuple[bool, Optional[str]]:
"""
Validate a Python package name.
Rules:
- Must start with a letter or number
- Can contain letters, numbers, underscores, hyphens, dots
- Cannot start or end with underscore, hyphen, or dot
- Cannot have consecutive special characters
- Case insensitive (normalized to lowercase)
Returns:
(is_valid, error_message or None)
Examples:
>>> validate_package_name("my-package")
(True, None)
>>> validate_package_name("-invalid")
(False, "Cannot start with hyphen")
"""
# YOUR CODE HERE
pass
# Test
names = [
"my-package",
"my_package",
"package123",
"-invalid",
"invalid-",
"123package",
"My.Package",
"__invalid__",
"a",
"valid_name-123",
]
# for name in names:
# valid, error = validate_package_name(name)
# print(f"{name}: {'✓' if valid else f'✗ ({error})'}")
# ============================================================
# EXERCISE 6: Entry Points Generator
# ============================================================
"""
Generate entry point configurations for CLI commands.
"""
from typing import Dict
def generate_entry_points(
commands: Dict[str, str],
package_name: str
) -> Dict[str, str]:
"""
Generate entry point configuration.
Args:
commands: Dict mapping command name to function path
e.g., {"greet": "greet_command", "info": "info_command"}
package_name: Name of the package
Returns:
Dict with 'pyproject' and 'setup_py' keys containing
the respective configuration strings.
Example:
>>> cmds = {"my-cli": "cli:main", "my-tool": "tools:run"}
>>> result = generate_entry_points(cmds, "my_package")
>>> print(result['pyproject'])
[project.scripts]
my-cli = "my_package.cli:main"
my-tool = "my_package.tools:run"
"""
# YOUR CODE HERE
pass
# Test
# commands = {"greet": "cli:greet", "info": "cli:info", "run": "main:run"}
# result = generate_entry_points(commands, "my_package")
# print(result['pyproject'])
# ============================================================
# EXERCISE 7: MANIFEST.in Generator
# ============================================================
"""
Generate a MANIFEST.in file for including/excluding files
in source distributions.
"""
def generate_manifest(
include_patterns: List[str],
exclude_patterns: List[str],
recursive_include: Dict[str, List[str]],
) -> str:
"""
Generate MANIFEST.in content.
Args:
include_patterns: Files/patterns to include (e.g., ["LICENSE", "README.md"])
exclude_patterns: Files/patterns to exclude
recursive_include: Dict of directory -> patterns
e.g., {"docs": ["*.md", "*.rst"]}
Returns:
MANIFEST.in file content
Example:
>>> content = generate_manifest(
... include_patterns=["README.md", "LICENSE"],
... exclude_patterns=["*.pyc"],
... recursive_include={"docs": ["*.md"]}
... )
"""
# YOUR CODE HERE
pass
# Test
# manifest = generate_manifest(
# include_patterns=["README.md", "LICENSE", "CHANGELOG.md"],
# exclude_patterns=["*.pyc", "__pycache__"],
# recursive_include={"docs": ["*.md", "*.rst"], "examples": ["*.py"]}
# )
# print(manifest)
# ============================================================
# EXERCISE 8: License Selector
# ============================================================
"""
Generate license file content based on license type.
"""
from datetime import datetime
def generate_license(
license_type: str,
author: str,
year: Optional[int] = None
) -> str:
"""
Generate license file content.
Supported licenses: MIT, Apache-2.0, BSD-3-Clause, GPL-3.0
Args:
license_type: Type of license
author: Author/copyright holder name
year: Copyright year (defaults to current year)
Returns:
License file content
Raises:
ValueError: If license_type is not supported
"""
# YOUR CODE HERE
pass
# print(generate_license("MIT", "John Doe"))
# ============================================================
# EXERCISE 9: Changelog Generator
# ============================================================
"""
Generate and update CHANGELOG.md following Keep a Changelog format.
"""
from dataclasses import dataclass
from typing import List
from datetime import date
@dataclass
class ChangeEntry:
category: str # Added, Changed, Deprecated, Removed, Fixed, Security
description: str
@dataclass
class VersionEntry:
version: str
date: date
changes: List[ChangeEntry]
class Changelog:
"""
Changelog manager following Keep a Changelog format.
Example:
>>> cl = Changelog()
>>> cl.add_version("1.0.0", date.today(), [
... ChangeEntry("Added", "Initial release"),
... ChangeEntry("Added", "Core functionality"),
... ])
>>> print(cl.render())
"""
VALID_CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
def __init__(self):
# YOUR CODE HERE
pass
def add_version(self, version: str, release_date: date, changes: List[ChangeEntry]):
"""Add a new version entry."""
# YOUR CODE HERE
pass
def render(self) -> str:
"""Render changelog as markdown."""
# YOUR CODE HERE
pass
# Test
# cl = Changelog()
# cl.add_version("0.1.0", date(2024, 1, 15), [
# ChangeEntry("Added", "Initial project structure"),
# ChangeEntry("Added", "Core functionality"),
# ])
# cl.add_version("0.2.0", date(2024, 2, 1), [
# ChangeEntry("Added", "CLI interface"),
# ChangeEntry("Fixed", "Bug in data processing"),
# ])
# print(cl.render())
# ============================================================
# EXERCISE 10: Project Scaffolder
# ============================================================
"""
Create a complete project scaffolding tool that generates
all necessary files for a new Python package.
"""
from pathlib import Path
from typing import Optional
class ProjectScaffolder:
"""
Generate complete Python project structure.
Example:
>>> scaffolder = ProjectScaffolder(
... name="my-awesome-project",
... author="Jane Doe",
... email="jane@example.com",
... description="An awesome project"
... )
>>> files = scaffolder.generate()
>>> for path, content in files.items():
... print(f"Would create: {path}")
"""
def __init__(
self,
name: str,
author: str,
email: str,
description: str = "",
license_type: str = "MIT",
python_version: str = "3.8",
use_src_layout: bool = True,
include_cli: bool = False,
dependencies: Optional[List[str]] = None,
):
# YOUR CODE HERE
pass
def _package_name(self) -> str:
"""Convert project name to valid package name."""
# YOUR CODE HERE
pass
def _generate_pyproject(self) -> str:
"""Generate pyproject.toml content."""
# YOUR CODE HERE
pass
def _generate_init(self) -> str:
"""Generate __init__.py content."""
# YOUR CODE HERE
pass
def _generate_readme(self) -> str:
"""Generate README.md content."""
# YOUR CODE HERE
pass
def _generate_gitignore(self) -> str:
"""Generate .gitignore content."""
# YOUR CODE HERE
pass
def generate(self) -> Dict[str, str]:
"""
Generate all project files.
Returns:
Dict mapping file paths to their content
"""
# YOUR CODE HERE
pass
def write(self, base_path: Path):
"""Write all files to disk."""
# YOUR CODE HERE
pass
# Test
# scaffolder = ProjectScaffolder(
# name="my-cool-project",
# author="Jane Doe",
# email="jane@example.com",
# description="A very cool project",
# include_cli=True,
# dependencies=["requests", "click"],
# )
# files = scaffolder.generate()
# for path in sorted(files.keys()):
# print(f" {path}")
# ============================================================
# SOLUTIONS (Uncomment to check your work)
# ============================================================
"""
# Solution 2: SemanticVersion
class SemanticVersion:
def __init__(self, version_string: str):
parts = version_string.split('-')
main_version = parts[0]
self.prerelease = parts[1] if len(parts) > 1 else None
v_parts = main_version.split('.')
self.major = int(v_parts[0])
self.minor = int(v_parts[1]) if len(v_parts) > 1 else 0
self.patch = int(v_parts[2]) if len(v_parts) > 2 else 0
def _compare_tuple(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other):
return self._compare_tuple() == other._compare_tuple()
def __lt__(self, other):
return self._compare_tuple() < other._compare_tuple()
def __le__(self, other):
return self._compare_tuple() <= other._compare_tuple()
def __gt__(self, other):
return self._compare_tuple() > other._compare_tuple()
def __ge__(self, other):
return self._compare_tuple() >= other._compare_tuple()
def __str__(self):
base = f"{self.major}.{self.minor}.{self.patch}"
return f"{base}-{self.prerelease}" if self.prerelease else base
def is_compatible(self, other):
return self.major == other.major
# Solution 3: version_satisfies
def version_satisfies(version: str, specifier: str) -> bool:
def compare_versions(v1: str, v2: str) -> int:
parts1 = [int(x) for x in v1.split('.')]
parts2 = [int(x) for x in v2.split('.')]
# Pad shorter version with zeros
while len(parts1) < len(parts2):
parts1.append(0)
while len(parts2) < len(parts1):
parts2.append(0)
for p1, p2 in zip(parts1, parts2):
if p1 < p2:
return -1
if p1 > p2:
return 1
return 0
operators = {
'==': lambda v, s: compare_versions(v, s) == 0,
'!=': lambda v, s: compare_versions(v, s) != 0,
'>=': lambda v, s: compare_versions(v, s) >= 0,
'<=': lambda v, s: compare_versions(v, s) <= 0,
'>': lambda v, s: compare_versions(v, s) > 0,
'<': lambda v, s: compare_versions(v, s) < 0,
}
# Handle multiple specifiers (e.g., ">=1.0.0,<2.0.0")
for spec in specifier.split(','):
spec = spec.strip()
for op in ['>=', '<=', '==', '!=', '>', '<']:
if spec.startswith(op):
spec_version = spec[len(op):]
if not operators[op](version, spec_version):
return False
break
return True
# Solution 5: validate_package_name
def validate_package_name(name: str) -> tuple:
if not name:
return (False, "Name cannot be empty")
# Normalize
name_lower = name.lower()
# Check first character
if not name_lower[0].isalnum():
return (False, f"Cannot start with '{name[0]}'")
# Check last character
if not name_lower[-1].isalnum():
return (False, f"Cannot end with '{name[-1]}'")
# Check all characters
valid_chars = set('abcdefghijklmnopqrstuvwxyz0123456789_-.')
for i, char in enumerate(name_lower):
if char not in valid_chars:
return (False, f"Invalid character '{char}'")
# Check consecutive special characters
if char in '_-.' and i > 0 and name_lower[i-1] in '_-.':
return (False, "Cannot have consecutive special characters")
return (True, None)
# Solution 6: generate_entry_points
def generate_entry_points(commands: Dict[str, str], package_name: str) -> Dict[str, str]:
# pyproject.toml format
pyproject_lines = ["[project.scripts]"]
for cmd, path in commands.items():
pyproject_lines.append(f'{cmd} = "{package_name}.{path}"')
# setup.py format
setup_lines = ['entry_points={', ' "console_scripts": [']
for cmd, path in commands.items():
setup_lines.append(f' "{cmd}={package_name}.{path}",')
setup_lines.append(' ],')
setup_lines.append('},')
return {
'pyproject': '\\n'.join(pyproject_lines),
'setup_py': '\\n'.join(setup_lines),
}
"""
if __name__ == "__main__":
print("Python Packaging Exercises")
print("=" * 50)
print("\nComplete the exercises above to practice:")
print("- Parsing configuration files")
print("- Version comparison")
print("- Dependency resolution")
print("- Package name validation")
print("- Entry point generation")
print("- License file generation")
print("- Project scaffolding")
print("\nUncomment the solutions to check your work!")