python

exercises

exercises.py🐍
"""
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!")
Exercises - Python Tutorial | DeepML