Code Style#

Code style guidelines for Construbot contributions.

Python Style#

Follow PEP 8#

Code formatting:

  • 4 spaces for indentation (no tabs)

  • Max line length: 100 characters (not strict 79)

  • 2 blank lines between top-level functions/classes

  • 1 blank line between methods

Use Black:

black .

Use isort:

isort .

Naming Conventions#

# Variables and functions: snake_case
user_count = 10
def calculate_total():
    pass

# Classes: PascalCase
class ContractManager:
    pass

# Constants: UPPER_CASE
MAX_CONTRACTS = 100

# Private: _leading_underscore
def _internal_helper():
    pass

Type Hints#

Encouraged but not required:

def get_contracts(company: Company) -> QuerySet[Contrato]:
    return Contrato.objects.filter(company=company)

Docstrings#

Use Google style:

def create_contract(company, folio, amount):
    """Create a new contract.

    Args:
        company (Company): The company for this contract
        folio (str): Reference number
        amount (Decimal): Contract amount

    Returns:
        Contrato: The created contract

    Raises:
        ValidationError: If folio already exists
    """
    ...

Django Style#

Model Best Practices#

Always include company:

class MyModel(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)

Use verbose_name:

class Contrato(models.Model):
    folio = models.CharField(
        max_length=100,
        verbose_name="Reference Number",
        help_text="Unique identifier for this contract"
    )

Meta options:

class Meta:
    ordering = ['-created_at']
    unique_together = ('company', 'folio')
    verbose_name_plural = "Contracts"

View Best Practices#

Use class-based views:

from django.views.generic import ListView

class ContractListView(LoginRequiredMixin, ListView):
    model = Contrato
    template_name = 'contracts/list.html'

    def get_queryset(self):
        return Contrato.objects.filter(
            company=self.request.user.active_company
        )

Always scope to company:

# GOOD
contracts = Contrato.objects.filter(company=request.user.active_company)

# BAD
contracts = Contrato.objects.all()  # Leaks data!

Query Optimization#

Use select_related:

# Avoid N+1 queries
contracts = Contrato.objects.select_related(
    'contraparte',
    'sitio'
)

Use prefetch_related:

users = User.objects.prefetch_related('companies')

Security#

Never trust user input:

# Use Django forms for validation
form = ContractForm(request.POST)
if form.is_valid():
    contract = form.save()

Use get_object_or_404:

contract = get_object_or_404(Contrato, id=contract_id)

Check permissions:

if not request.user.nivel_acceso.nivel >= 3:
    raise PermissionDenied()

Testing Style#

Clear test names:

def test_contract_creation_with_valid_data():
    pass

def test_contract_creation_fails_with_duplicate_folio():
    pass

Use setUp for common data:

class ContractTestCase(TestCase):
    def setUp(self):
        self.company = Company.objects.create(...)

    def test_something(self):
        contract = Contrato.objects.create(company=self.company)

One assertion per test (ideally):

def test_contract_folio_is_uppercase(self):
    contract = Contrato.objects.create(folio="abc")
    self.assertEqual(contract.folio, "ABC")

Git Commit Messages#

Format:

Short summary (50 chars max)

More detailed explanation if needed. Wrap at 72 characters.

- List of changes
- Another change

Good examples:

Add hierarchical contract support

Implement django-treebeard for parent/child contract relationships.
Includes migration, model updates, and tree query methods.

Fix: Prevent data leak in contract list view

Ensure contracts are filtered by active_company before display.

Bad examples:

Fixed stuff
WIP
asdf
Updated code

Common Patterns#

Factory pattern for tests:

class ContractFactory:
    @staticmethod
    def create(**kwargs):
        defaults = {
            'company': CompanyFactory.create(),
            'folio': 'C-001',
            'monto': Decimal('1000000.00'),
        }
        defaults.update(kwargs)
        return Contrato.objects.create(**defaults)

Context manager for transactions:

from django.db import transaction

with transaction.atomic():
    contract = Contrato.objects.create(...)
    estimate = Estimate.objects.create(contract=contract, ...)

Queryset methods:

class ContratoQuerySet(models.QuerySet):
    def for_company(self, company):
        return self.filter(company=company)

    def active(self):
        return self.filter(status='active')

Tools#

Linting:

# pylint
pylint construbot/

# flake8
flake8 construbot/

Formatting:

# Black (code formatter)
black .

# isort (import sorting)
isort .

Type checking (optional):

mypy construbot/

Pre-commit Hooks#

Recommended .pre-commit-config.yaml:

repos:
  - repo: https://github.com/psf/black
    rev: 22.10.0
    hooks:
      - id: black

  - repo: https://github.com/pycqa/isort
    rev: 5.10.1
    hooks:
      - id: isort

  - repo: https://github.com/pycqa/flake8
    rev: 5.0.4
    hooks:
      - id: flake8

See Also#