Chapter 6: Imports System and File Operations#
As your projects grow beyond a single file, keeping your code organized becomes essential for maintainability and collaboration. A well-structured project is easier to understand, test, and scale. This chapter introduces Jac's module system for organizing code across multiple files and its familiar approach to file operations.
You will learn how to import your own code, leverage the vast Python ecosystem, and use Jac's powerful interface-implementation pattern to build clean, robust applications.
Module Organization Philosophy
Jac's import system is your primary tool for structuring code. It allows you to define objects, nodes, walkers, and functions in separate files and use them wherever they are needed. Jac seamlessly integrates with the Python ecosystem, allowing you to import and use Python libraries directly.
Import Statements and Module Organization#
Basic Import Patterns#
Basic Import Statements
# Import Python modules
import os;
import json;
import sys;
# Import specific functions from Python modules
import from datetime {datetime}
import from pathlib {Path}
# Import Jac modules
# include my_module;
# include utils.file_helper;
with entry {
# Use imported modules
current_time = datetime.now();
current_dir = os.getcwd();
print(f"Current time: {current_time}");
print(f"Current directory: {current_dir}");
}
# Import Python modules
import os
import json
import sys
# Import specific functions
from datetime import datetime
from pathlib import Path
# Import local modules
import my_module
from utils import file_helper
if __name__ == "__main__":
# Use imported modules
current_time = datetime.now()
current_dir = os.getcwd()
print(f"Current time: {current_time}")
print(f"Current directory: {current_dir}")
Implementation Separation#
Jac encourages a clean architectural pattern that separates what a component does from how it does it. This is achieved by splitting an object's or node's definition (its interface) from its method logic (its implementation).
The interface is defined in a .jac
file, while the implementation is placed in a corresponding .impl.jac
file. When you import the object, Jac automatically links them together.
Benefits of Separation
This pattern makes your code significantly more maintainable. You can change the internal logic of a method in the .impl.jac
file without ever touching the files that use the object. It also makes testing easier, as you can mock implementations while testing against a stable interface.
Interface and Implementation Separation
# Implementation file
impl Calculator.add {
result = a + b;
return round(result, self.precision);
}
impl Calculator.subtract {
result = a - b;
return round(result, self.precision);
}
impl Calculator.multiply {
result = a * b;
return round(result, self.precision);
}
impl Calculator.divide {
if b == 0.0 {
raise ValueError("Division by zero");
}
result = a / b;
return round(result, self.precision);
}
# Python class definition
class Calculator:
def __init__(self, precision: int = 2):
self.precision = precision
def add(self, a: float, b: float) -> float:
result = a + b
return round(result, self.precision)
def subtract(self, a: float, b: float) -> float:
result = a - b
return round(result, self.precision)
def multiply(self, a: float, b: float) -> float:
result = a * b
return round(result, self.precision)
def divide(self, a: float, b: float) -> float:
if b == 0.0:
raise ValueError("Division by zero")
result = a / b
return round(result, self.precision)
Namespace Injection#
Namespace Injection
Jac provides several mechanisms to manage namespaces clearly and effectively:
- import: Loads an entire Python module or package, preserving its namespace.
- include: Imports all exported symbols from a Jac module directly into the current namespace, flattening it and simplifying access.
- import from: Explicitly imports selected symbols from a module, improving clarity and avoiding namespace pollution.
- Aliasing: Allows renaming imported modules or symbols, helping avoid naming conflicts.
Jac Import Internals#
Import Resolution Workflow
Jac resolves imports using a structured process:
- Parses import statements to determine modules.
- Searches for modules in the caller directory,
JAC_PATH
, and Python'ssys.path
. - Compiles
.jac
files to bytecode (.jir
) if necessary. - Executes bytecode to populate module namespaces.
- Caches modules to improve performance.
Common issues include missing bytecode, syntax errors, and circular dependencies.
File Operations and External Integration#
File Handling
File operations are essential for configuration management, data processing, and system integration.
Basic File Operations#
File Reading and Writing
import os;
import json;
# Read text file safely
def read_file(filepath: str) -> str | None {
try {
with open(filepath, 'r') as file {
return file.read();
}
} except FileNotFoundError {
print(f"File not found: {filepath}");
return None;
} except Exception as e {
print(f"Error reading file: {e}");
return None;
}
}
# Write text file safely
def write_file(filepath: str, content: str) -> bool {
try {
with open(filepath, 'w') as file {
file.write(content);
}
return True;
} except Exception as e {
print(f"Error writing file: {e}");
return False;
}
}
# Read JSON file
def read_json(filepath: str) -> dict | None {
try {
with open(filepath, 'r') as file {
return json.load(file);
}
} except FileNotFoundError {
print(f"JSON file not found: {filepath}");
return None;
} except json.JSONDecodeError {
print(f"Invalid JSON in file: {filepath}");
return None;
}
}
with entry {
# Test file operations
test_content = "Hello from Jac!";
if write_file("test.txt", test_content) {
content = read_file("test.txt");
print(f"File content: {content}");
}
}
import os
import json
from typing import Optional
# Read text file safely
def read_file(filepath: str) -> Optional[str]:
try:
with open(filepath, 'r') as file:
return file.read()
except FileNotFoundError:
print(f"File not found: {filepath}")
return None
except Exception as e:
print(f"Error reading file: {e}")
return None
# Write text file safely
def write_file(filepath: str, content: str) -> bool:
try:
with open(filepath, 'w') as file:
file.write(content)
return True
except Exception as e:
print(f"Error writing file: {e}")
return False
# Read JSON file
def read_json(filepath: str) -> Optional[dict]:
try:
with open(filepath, 'r') as file:
return json.load(file)
except FileNotFoundError:
print(f"JSON file not found: {filepath}")
return None
except json.JSONDecodeError:
print(f"Invalid JSON in file: {filepath}")
return None
if __name__ == "__main__":
# Test file operations
test_content = "Hello from Python!"
if write_file("test.txt", test_content):
content = read_file("test.txt")
print(f"File content: {content}")
Complete Example: Configuration Management System#
Multi-Module Application
This example demonstrates how to build a configuration system using multiple modules working together.
Configuration Reader Module#
Configuration Reader (config_reader.jac)
# config_reader.jac
import json;
import os;
import from pathlib { Path }
obj ConfigReader {
has config_file: str;
has config_data: dict[str, any] = {};
def load_config() -> bool;
def get_value(key: str, default: any = None) -> any;
def set_value(key: str, value: any) -> None;
def save_config() -> bool;
def create_default_config() -> None;
}
impl ConfigReader.load_config {
if not os.path.exists(self.config_file) {
print(f"Config file {self.config_file} not found, creating default");
self.create_default_config();
return True;
}
try {
with open(self.config_file, 'r') as file {
self.config_data = json.load(file);
}
print(f"Config loaded from {self.config_file}");
return True;
} except json.JSONDecodeError {
print(f"Invalid JSON in {self.config_file}");
return False;
} except Exception as e {
print(f"Error loading config: {e}");
return False;
}
}
impl ConfigReader.get_value {
return self.config_data.get(key, default);
}
impl ConfigReader.set_value {
self.config_data[key] = value;
}
impl ConfigReader.save_config {
try {
with open(self.config_file, 'w') as file {
json.dump(self.config_data, file, indent=2);
}
print(f"Config saved to {self.config_file}");
return True;
} except Exception as e {
print(f"Error saving config: {e}");
return False;
}
}
impl ConfigReader.create_default_config {
self.config_data = {
"app_name": "My Jac App",
"version": "1.0.0",
"debug": False,
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db"
},
"logging": {
"level": "INFO",
"file": "app.log"
}
};
self.save_config();
}
# config_reader.py
import json
import os
from pathlib import Path
from typing import Any, Dict, Optional
class ConfigReader:
def __init__(self, config_file: str):
self.config_file = config_file
self.config_data: Dict[str, Any] = {}
def load_config(self) -> bool:
if not os.path.exists(self.config_file):
print(f"Config file {self.config_file} not found, creating default")
self.create_default_config()
return True
try:
with open(self.config_file, 'r') as file:
self.config_data = json.load(file)
print(f"Config loaded from {self.config_file}")
return True
except json.JSONDecodeError:
print(f"Invalid JSON in {self.config_file}")
return False
except Exception as e:
print(f"Error loading config: {e}")
return False
def get_value(self, key: str, default: Any = None) -> Any:
return self.config_data.get(key, default)
def set_value(self, key: str, value: Any) -> None:
self.config_data[key] = value
def save_config(self) -> bool:
try:
with open(self.config_file, 'w') as file:
json.dump(self.config_data, file, indent=2)
print(f"Config saved to {self.config_file}")
return True
except Exception as e:
print(f"Error saving config: {e}")
return False
def create_default_config(self) -> None:
self.config_data = {
"app_name": "My Python App",
"version": "1.0.0",
"debug": False,
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db"
},
"logging": {
"level": "INFO",
"file": "app.log"
}
}
self.save_config()
Application Module#
Application Module (app.jac)
# app.jac
# include config_reader;
import logging;
obj Application {
has config: ConfigReader;
has logger: any;
def start() -> None;
def setup_logging() -> None;
def get_database_config() -> dict[str, any];
def run_debug_mode() -> None;
def run_normal_mode() -> None;
}
impl Application.start {
print("=== Starting Application ===");
# Load configuration
if self.config.load_config() {
self.setup_logging();
# Display app info
app_name = self.config.get_value("app_name", "Unknown App");
version = self.config.get_value("version", "1.0.0");
debug_mode = self.config.get_value("debug", False);
print(f"App: {app_name} v{version}");
print(f"Debug mode: {debug_mode}");
# Show database config
db_config = self.get_database_config();
print(f"Database: {db_config['host']}:{db_config['port']}/{db_config['name']}");
if debug_mode {
self.run_debug_mode();
} else {
self.run_normal_mode();
}
} else {
print("Failed to load configuration");
}
}
impl Application.setup_logging {
log_config = self.config.get_value("logging", {});
log_level = log_config.get("level", "INFO");
log_file = log_config.get("file", "app.log");
logging.basicConfig(
level=getattr(logging, log_level),
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
);
self.logger = logging.getLogger("app");
self.logger.info("Logging configured");
}
impl Application.get_database_config {
default_db = {"host": "localhost", "port": 5432, "name": "default_db"};
return self.config.get_value("database", default_db);
}
impl Application.run_debug_mode {
print(">>> Running in DEBUG mode");
print(f">>> Full config: {self.config.config_data}");
}
impl Application.run_normal_mode {
print(">>> Running in NORMAL mode");
print(">>> Application ready");
}
# app.py
from config_reader import ConfigReader
import logging
from typing import Dict, Any
class Application:
def __init__(self, config_file: str):
self.config = ConfigReader(config_file)
self.logger = None
def start(self) -> None:
print("=== Starting Application ===")
# Load configuration
if self.config.load_config():
self.setup_logging()
# Display app info
app_name = self.config.get_value("app_name", "Unknown App")
version = self.config.get_value("version", "1.0.0")
debug_mode = self.config.get_value("debug", False)
print(f"App: {app_name} v{version}")
print(f"Debug mode: {debug_mode}")
# Show database config
db_config = self.get_database_config()
print(f"Database: {db_config['host']}:{db_config['port']}/{db_config['name']}")
if debug_mode:
self.run_debug_mode()
else:
self.run_normal_mode()
else:
print("Failed to load configuration")
def setup_logging(self) -> None:
log_config = self.config.get_value("logging", {})
log_level = log_config.get("level", "INFO")
log_file = log_config.get("file", "app.log")
logging.basicConfig(
level=getattr(logging, log_level),
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
self.logger = logging.getLogger("app")
self.logger.info("Logging configured")
def get_database_config(self) -> Dict[str, Any]:
default_db = {"host": "localhost", "port": 5432, "name": "default_db"}
return self.config.get_value("database", default_db)
def run_debug_mode(self) -> None:
print(">>> Running in DEBUG mode")
print(f">>> Full config: {self.config.config_data}")
def run_normal_mode(self) -> None:
print(">>> Running in NORMAL mode")
print(">>> Application ready")
Main Application Entry Point#
Main Entry Point (main.jac)
# main.jac
include app;
with entry {
print("=== Configuration Management Demo ===");
# Create and run application
application = Application(config=ConfigReader(config_file="app_config.json"));
application.start();
print("\n=== Configuration Update Demo ===");
# Update configuration at runtime
application.config.set_value("debug", True);
application.config.set_value("app_name", "Updated Jac App");
application.config.save_config();
# Restart with new config
print("\nRestarting with updated configuration:");
application.start();
}
# main.py
from app import Application
if __name__ == "__main__":
print("=== Configuration Management Demo ===")
# Create and run application
application = Application("app_config.json")
application.start()
print("\n=== Configuration Update Demo ===")
# Update configuration at runtime
application.config.set_value("debug", True)
application.config.set_value("app_name", "Updated Python App")
application.config.save_config()
# Restart with new config
print("\nRestarting with updated configuration:")
application.start()
Package Structure and Organization#
Project Structure
Well-organized project structure makes your code maintainable and scalable.
Recommended Project Structure
my_jac_project/
├── main.jac # Main entry point
├── app.jac # Application logic
├── app.test.jac # App tests
├── config_reader.jac # Config management
├── config_reader.impl.jac # Config implementation
├── config_reader.test.jac # Config tests
├── utils/
│ ├── file_utils.jac # File utilities
│ └── data_utils.jac # Data processing
├── models/
│ ├── user.jac # User model
│ └── user.impl.jac # User implementation
├── docs/
│ └── README.md # Documentation
└── config/
└── app_config.json # Configuration files
my_python_project/
├── main.py # Main entry point
├── app.py # Application logic
├── config_reader.py # Config management
├── utils/
│ ├── __init__.py
│ ├── file_utils.py # File utilities
│ └── data_utils.py # Data processing
├── models/
│ ├── __init__.py
│ └── user.py # User model
├── tests/
│ ├── __init__.py
│ ├── test_config.py # Config tests
│ └── test_app.py # App tests
├── docs/
│ └── README.md # Documentation
└── config/
└── app_config.json # Configuration files
Best Practices#
Import and File Operation Best Practices
- Organize by functionality: Group related code into logical modules
- Use explicit imports: Import only what you need for clarity
- Handle errors gracefully: Always use try-catch for file operations
- Separate interface from implementation: Use
.impl.jac
files for complex objects - Validate file inputs: Check file existence and format before processing
- Use configuration files: Externalize settings for flexibility
- Document your modules: Clear documentation helps team collaboration
Key Takeaways#
What We've Learned
Import System:
- Python integration: Seamless access to Python modules and libraries
- Namespace management: Clear control over imported symbols and namespaces
- Aliasing support: Rename imports to avoid conflicts and improve readability
- Selective imports: Import specific functions and classes for better organization
Module Organization:
- Implementation separation:
.impl.jac
files promote clean architecture - Interface definitions: Clear separation between public interfaces and implementations
- Namespace injection: Various mechanisms for managing symbol visibility
- Dependency management: Structured approach to module dependencies
File Operations:
- Safe file handling: Robust error handling for file operations
- JSON processing: Built-in support for configuration and data files
- Path management: Integration with Python's pathlib for file system operations
- Configuration management: External configuration files for application flexibility
Project Structure:
- Modular design: Logical organization of code into focused modules
- Testing integration: Built-in support for test files alongside implementation
- Documentation: Clear structure for maintaining project documentation
- Scalability: Structure that grows with project complexity
Try It Yourself
Build a modular application by: - Creating a multi-file configuration system - Implementing interface/implementation separation - Setting up a proper project structure - Adding error handling for file operations
Remember: Well-organized modules make your code maintainable and scalable!
Your code is now well-organized and modular. Let's enhance it further with Jac's powerful object-oriented features!