1
\$\begingroup\$

I have recently been trying to improve the quality of my code. Towards this goal I want to start writing unit tests and I also am trying to implement things i have read as best practices. I have read that its better to have a class raise exceptions over returning error codes. In the below class I have an init method that has dependency requirements based on other arguments being passed in.

Extra details... The class is from a contracts module that has 3 classes. Contract, ContractLine, ContractCustomer. Contracts can be of two types ('item','vendor') and a contract can also be a group contract. Group contracts are one contract shared amount many customers. A "normal" or non group contract would only have one customer these are all sqlalchemy declarative classes. TimeUserMixin is a class that provides create_time,modify_time, create_user,modify_user to classes where i want to track who made changes and when the changes were made.

My Questions ...

  1. Are exceptions the right way to go or is there a "better" way to do what I am trying to do.
  2. How would I write a tests for this init method?
  3. Is it a bad idea to pass in other objects to my classes verse passing in the objects id(db id)?

      class Contract(DeclarativeBase,TimeUserMixin):
            """
            Contract definition.
    
            This is the customer contract definition.
            Valid contract_type's = 'item'|'vendor'
    
            """
            __tablename__ = 'contracts'
    
            contract_id = Column(Integer, primary_key=True,autoincrement=True)
    
            code = Column(Unicode(25),nullable=False)
    
            description = Column(Unicode(100),nullable=False)
    
            notes = Column(Unicode())
    
            is_group_contract = Column(Boolean,nullable=False,default=False)
    
    
            contract_type = Column(Unicode(20))
    
            start_date = Column(Date,nullable=False,default=datetime.date.today)
    
            end_date = Column(Date,nullable=False)
    
            reminder_date = Column(Date,nullable=False)
    
            discount_perc = Column(Numeric(precision=5,scale=4),nullable=False,default=Decimal('0.0'))
    
            vendor_id = Column(Integer,ForeignKey('vendors.vendor_id'))
    
            vendor = relation('Vendor',backref=backref('contracts'),
                              primaryjoin='Contract.vendor_id == Vendor.vendor_id')
    
            _CONTRACT_TYPES = ('item','vendor')
    
            @property
            def customer(self):
                """
                Returs Contracts Customer
    
                Returns the customer object if contract is not a group contract.
                If contract is a group contract raise exception AttributeError
    
                """
                if self.is_group_contract:
                    raise AttributeError('customer property not avaible on group contracts.')
                return self.customers[0]
    
            def add_line(self,item,price,min_qty=None):
                """Adds a new contract line to the contract."""
                new_line = ContractLine(self,item,price,min_qty)
                DBSession.add(new_line)
    
            def add_customer(self,customer):
                """
                Adds a customer to a contract.
    
                customer -- Customer object
                Raises TypeError if not a group contract and a cutomer is already assigned.
    
                """
                if not self.is_group_contract and \
                        len(self.customers) > 1:
                    raise TypeError('Contract is not a group contract, and already has a customer assigned.')
                new_customer = ContractCustomer(self,customer)
                DBSession.add(new_customer)
    
            def __init__(self,code,description,contract_type,
                         start_date,end_date,reminder_date,
                         customer=None,isgroup=False,vendor=None,
                         discount_perc=None):
                """
                Create new contract
    
                Requires 'contract_type' ['item','vendor']
                if not a group contract requires customer object
                if vendor contract requires vendor object and discount perc
    
                """
                self.code = code
                self.description = description
                self.contract_type = contract_type
                self.start_date = start_date
                self.end_date = end_date
                self.reminder_date = reminder_date
                if contract_type not in self._CONTRACT_TYPES:
                    raise AttributeError("Valid contract types are 'item' & 'vendor'")
                if isgroup:
                    if customer:
                        raise AttributeError("Group contracts should not have 'customer' passed in")
                    self.is_group_contract = True
                else:
                    if customer:
                        self.add_customer(customer)
                    else:
                        raise AttributeError('Customer required for non group contracts.')
                if contract_type == 'vendor':
                    if vendor and discount_perc:
                        self.vendor = vendor
                        self.discount_perc = discount_perc
                    else:
                        if not vendor:
                            raise AttributeError('Vendor contracts require vendor to be passed in')
                        if not discount_perc:
                            raise AttributeError('Vendor contracts reqire discount_perc(Decimal)')
    
        class ContractLine(DeclarativeBase,TimeUserMixin):
            """Contract Line Definition"""
    
            __tablename__ = 'contract_lines'
    
            line_id = Column(Integer, primary_key=True,autoincrement=True)
    
            min_qty = Column(Integer,default=0,nullable=False)
    
            contract_price = Column(Numeric(precision=13,scale=2))
    
            contract_id = Column(Integer,ForeignKey('contracts.contract_id'),nullable=False)
    
            contract = relation(Contract,backref=backref('lines'))
    
            item_id = Column(Integer,ForeignKey('items.item_id'),nullable=False)
    
            item = relation('Item',backref=backref('contract_lines'))
    
            def __init__(self,contract,item,price,min_qty=None):
                """Create new contract line"""
                self.contract = contract
                self.item = item
                self.contract_price = price
                if min_qty:
                    self.min_qty = min_qty
    
        class ContractCustomer(DeclarativeBase):
            """
            Contract customer definition
    
            Private class should only be accessed from Contract class
    
            """
            __tablename__ = 'contract_customers'
    
            contract_customer_id = Column(Integer, primary_key=True,autoincrement=True)
    
            contract_id = Column(Integer,ForeignKey('contracts.contract_id'),nullable=False)
    
            contract = relation(Contract,backref=backref('customers'),
                                primaryjoin="ContractCustomer.contract_id == Contract.contract_id")
    
            customer_id = Column(Integer,ForeignKey('customers.customer_id'))
    
            customer = relation(m_ar.Customer,backref=backref('contracts'),
                                primaryjoin='Customer.customer_id==ContractCustomer.customer_id')
    
            def __init__(self,contract,customer):
                """
                Assigns customer to contract
    
                contract -- contract object
                customer -- customer object
    
                """
                self.contract = contract
                self.customer = customer
    
\$\endgroup\$
1
  • \$\begingroup\$ I can't seem to get the code block to format correctly. \$\endgroup\$
    – Ominus
    Feb 13, 2012 at 16:15

1 Answer 1

2
\$\begingroup\$

You raise odd exceptions. TypeError should be raised when the type of an object is incorrect. But you are raising for not following business rules. AttributeError should be raised when an attribute is not available, not when you fail validation. You should really either raise your own exceptions or use ValueError.

You write unit tests by calling the constructors with different arguments and making sure the correct exceptions get thrown. Where are you having trouble?

contract_type is a type parameter. That's a sign that you should really have separate classes for the two types of contracts. You should really have a VendorContract and and ItemContract.

The customer thing doesn't feel right. The class basically modifies its interface depending on whether or not its a group contract. Do you actually need to modify the interface? Can you simply say that Contracts with multiple customers are group contracts, and not separately keep track of whether its a group contract? Does other code distinguish between group and non-group contracts? I don't like they way you've done it here, but without more information I'm not sure of a better way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.