"""
================================================================================
File: 02_custom_modules.py
Topic: Creating and Using Custom Modules
================================================================================

This file demonstrates how to create your own modules and packages in Python.
Custom modules help organize code, promote reusability, and maintain clean
project structures.

Key Concepts:
- Creating module files
- The __name__ variable
- __init__.py for packages
- Relative imports
- Module documentation
- Publishing modules

================================================================================
"""

# -----------------------------------------------------------------------------
# 1. What is a Module?
# -----------------------------------------------------------------------------
# A module is simply a .py file containing Python code

print("--- What is a Module? ---")

# Any Python file is a module!
# If you have: my_utils.py
# You can: import my_utils

# Example module content (imagine this is saved as 'my_math.py'):
"""
# my_math.py

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

PI = 3.14159

class Calculator:
    def multiply(self, a, b):
        return a * b
"""

print("A module is just a Python file that can be imported.")
print("It can contain functions, classes, and variables.")

# -----------------------------------------------------------------------------
# 2. The __name__ Variable
# -----------------------------------------------------------------------------
# __name__ tells you how the module is being used

print("\n--- The __name__ Variable ---")

# When a file is run directly: __name__ == "__main__"
# When a file is imported: __name__ == module_name

print(f"Current __name__: {__name__}")

# Common pattern for executable modules:
"""
# utils.py

def useful_function():
    return "I'm useful!"

def main():
    print("Running as main program")
    print(useful_function())

# This block only runs when file is executed directly
if __name__ == "__main__":
    main()
"""

# Demo of the pattern
def demo_function():
    """A function that would be in a module."""
    return "Hello from demo!"

def main():
    """Main function that runs when executed directly."""
    print("This module is being run directly!")
    print(demo_function())

# This is the __name__ check pattern
if __name__ == "__main__":
    # This runs only when this file is executed directly
    # Not when it's imported
    pass  # In real code, you'd call main()

print("\nThe 'if __name__ == \"__main__\"' pattern prevents code from")
print("running when the module is imported.")

# -----------------------------------------------------------------------------
# 3. Module Structure Example
# -----------------------------------------------------------------------------

print("\n--- Module Structure ---")

# A well-structured module typically has:
module_template = '''
"""
Module: my_module
Description: What this module does

This module provides functionality for...
"""

# Imports at the top
import os
from typing import List

# Module-level constants
VERSION = "1.0.0"
DEFAULT_TIMEOUT = 30

# Private helpers (convention: prefix with _)
def _internal_helper():
    """Not meant to be used outside this module."""
    pass

# Public functions
def public_function(arg1: str) -> str:
    """
    This is a public function.
    
    Args:
        arg1: Description of argument
    
    Returns:
        Description of return value
    """
    return f"Result: {arg1}"

# Classes
class MyClass:
    """A class in the module."""
    pass

# Main entry point (if applicable)
def main():
    """Entry point when run as script."""
    print(public_function("test"))

if __name__ == "__main__":
    main()
'''

print("A module should have:")
print("  1. Module docstring at top")
print("  2. Imports")
print("  3. Constants")
print("  4. Private helpers (prefixed with _)")
print("  5. Public functions/classes")
print("  6. Main block if it's executable")

# -----------------------------------------------------------------------------
# 4. Creating a Package
# -----------------------------------------------------------------------------

print("\n--- Creating a Package ---")

# A package is a directory containing:
# - __init__.py (can be empty)
# - One or more module files

package_structure = """
my_package/
    __init__.py
    module1.py
    module2.py
    utils/
        __init__.py
        helpers.py
        validators.py
"""

print("Package structure:")
print(package_structure)

# The __init__.py file makes a directory a package
# It can be empty or contain initialization code

init_example = '''
# my_package/__init__.py

# Package version
__version__ = "1.0.0"

# Control what gets imported with "from my_package import *"
__all__ = ['module1', 'module2', 'important_function']

# Import commonly used items for convenient access
from .module1 import important_function
from .module2 import AnotherClass
'''

print("__init__.py can:")
print("  - Define package version")
print("  - Control __all__ exports")
print("  - Import frequently used items for convenience")

# -----------------------------------------------------------------------------
# 5. Import Examples for Packages
# -----------------------------------------------------------------------------

print("\n--- Package Import Examples ---")

import_examples = """
# Given this package structure:
# myapp/
#     __init__.py
#     core.py
#     utils/
#         __init__.py
#         helpers.py

# Import the package
import myapp

# Import a module from the package
from myapp import core

# Import a function from a module
from myapp.core import process_data

# Import from nested package
from myapp.utils import helpers
from myapp.utils.helpers import format_output

# Relative imports (inside the package)
# In myapp/core.py:
from . import utils           # Same-level import
from .utils import helpers    # Nested module
from .utils.helpers import format_output
from .. import other_package  # Parent-level (if applicable)
"""

print("Package import patterns:")
print("  import package")
print("  from package import module")
print("  from package.module import function")
print("  from package.sub import submodule")

# -----------------------------------------------------------------------------
# 6. Simulating a Module
# -----------------------------------------------------------------------------

print("\n--- Simulated Module Example ---")

# Let's create module-like code inline to demonstrate

# === This would be in: calculator.py ===
class Calculator:
    """A simple calculator class."""
    
    def __init__(self):
        self.history = []
    
    def add(self, a, b):
        """Add two numbers."""
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result
    
    def subtract(self, a, b):
        """Subtract b from a."""
        result = a - b
        self.history.append(f"{a} - {b} = {result}")
        return result
    
    def get_history(self):
        """Get calculation history."""
        return self.history

# === Using the "module" ===
calc = Calculator()
print(f"5 + 3 = {calc.add(5, 3)}")
print(f"10 - 4 = {calc.subtract(10, 4)}")
print(f"History: {calc.get_history()}")

# -----------------------------------------------------------------------------
# 7. Module Documentation
# -----------------------------------------------------------------------------

print("\n--- Module Documentation ---")

# Good module documentation includes:
# - Module-level docstring
# - Function/class docstrings
# - Type hints
# - Examples

documented_module = '''
"""
mymodule - A demonstration of proper documentation

This module provides utilities for string manipulation
and validation. It follows Google-style docstrings.

Example:
    >>> from mymodule import validate_email
    >>> validate_email("test@example.com")
    True

Attributes:
    EMAIL_REGEX (str): Regular expression for email validation
"""

import re
from typing import Optional

EMAIL_REGEX = r'^[\w.-]+@[\w.-]+\.\w+$'

def validate_email(email: str) -> bool:
    """
    Validate an email address format.
    
    Args:
        email: The email address to validate
    
    Returns:
        True if valid format, False otherwise
    
    Raises:
        TypeError: If email is not a string
    
    Example:
        >>> validate_email("user@domain.com")
        True
        >>> validate_email("invalid-email")
        False
    """
    if not isinstance(email, str):
        raise TypeError("email must be a string")
    return bool(re.match(EMAIL_REGEX, email))
'''

print("Include in your modules:")
print("  - Module docstring explaining purpose")
print("  - Type hints for parameters and returns")
print("  - Examples in docstrings")
print("  - Proper exception documentation")

# -----------------------------------------------------------------------------
# 8. The __all__ Variable
# -----------------------------------------------------------------------------

print("\n--- The __all__ Variable ---")

# __all__ controls what's exported with "from module import *"

all_example = '''
# mymodule.py

# Only these will be exported with "from mymodule import *"
__all__ = ['public_func', 'PublicClass', 'CONSTANT']

CONSTANT = 42

def public_func():
    """This is meant to be used externally."""
    pass

def _private_func():
    """This is internal (won't be exported)."""
    pass

class PublicClass:
    """This class is for external use."""
    pass
'''

print("__all__ defines what gets exported with 'import *'")
print("Items NOT in __all__ won't be exported")
print("Underscore-prefixed items are convention for private")

# -----------------------------------------------------------------------------
# 9. Reloading Modules
# -----------------------------------------------------------------------------

print("\n--- Reloading Modules ---")

# During development, you might need to reload a modified module
from importlib import reload

# If you modify 'mymodule', you can reload it:
# import mymodule
# # ... modify the file ...
# reload(mymodule)  # Get the updated version

print("Use importlib.reload() to reload a modified module")
print("Useful during development and debugging")

# -----------------------------------------------------------------------------
# 10. Project Structure Best Practices
# -----------------------------------------------------------------------------

print("\n--- Project Structure Best Practices ---")

project_structure = """
myproject/
    README.md
    setup.py or pyproject.toml
    requirements.txt
    .gitignore
    
    src/
        mypackage/
            __init__.py
            core.py
            utils.py
            models/
                __init__.py
                user.py
                product.py
    
    tests/
        __init__.py
        test_core.py
        test_utils.py
    
    docs/
        index.md
    
    scripts/
        run_server.py
"""

print("Recommended project structure:")
print(project_structure)

print("Key points:")
print("  - Separate source code (src/) from tests")
print("  - Keep configuration at project root")
print("  - Use __init__.py for all packages")
print("  - Tests mirror source structure")
