I have the following code in a standalone module, with doctest included at the class level.
If I run this as 'main' (i.e. from the command line for example), with doctest disabled, it runs just fine, and the prints at the end of the file give exactly what they should.
20120
20220
203820
But when I call the doctest at the beginning, then it seems to mess up the variables, and the results are not correct anymore. It seems that the doctest interferes with the variables, even if a new instance of BillNumberGenerator is instanciated after the doctest.
The doctest itself also fails, which is weird, because there is no bug in the logic of the functions/classes.
Am I missing something? Does doctest run in a parallel thread, gaining access to "production" variables maybe?
This seems very obscure to me. The problem was the same when I had the doctests inside relevant class functions.
from datetime import date
import doctest
class BillNumberGenerator():
"""
The bill number is a running counter (starting at 1 every year)
and is inserted in the middle of the year number.
E.g.: 14th bill of 2019 -> 20(14)19 -> 201119
>>> bng = BillNumberGenerator()
>>> print(bng.generate_number(date(2020, 1, 1)))
20120
>>> print(bng.generate_number(date(2020, 1, 1)))
20220
>>> bng.set_next_counter_for_year(2020, 111)
>>> print(bng.generate_number(date(2020, 1, 1)))
2011120
>>> print(bng.generate_number(date(2019, 1, 1)))
20119
>>> BillNumberGenerator.extract_counter_from_number('2011119')
111
>>> BillNumberGenerator.extract_counter_from_number('2019')
>>> BillNumberGenerator.extract_counter_from_number('text')
Returns (HTVA, TVA, TTC)
>>> b = Bill(date(2020, 1, 1), '', '', bng)
>>> b.add_item('Test', 117)
>>> b.add_item('Test', 50, tva=False)
>>> b.add_item('Test', 117)
>>> b.calculate_total()
(250.0, 34.0, 284.0)
"""
def __init__(self, dict_={}):
self._counters = dict_ # {int: int}
self.fresh = True
def generate_number(self, date:date) -> str:
self.fresh = False
prefix, suffix = map(str, divmod(date.year, 100))
next_counter = self._counters.get(date.year, 1)
self._counters[date.year] = next_counter + 1
return prefix + str(next_counter) + suffix
def set_next_counter_for_year(self, year:int, counter:int):
self.fresh = False
self._counters[year] = counter
@staticmethod
def extract_counter_from_number(bill_number:str) -> int:
try:
return counter if (counter:=int(bill_number[2:-2])) != 0 else None
except ValueError:
return None
class Bill():
def __init__(self, date, client, therapist, bng, tva_rate=0.17):
self.date = date
self.client = client
self.therapist = therapist
self.number = bng.generate_number(date)
self.items = [] # list of tuples (item:str, price:float, tva:bool)
self.tva_rate = tva_rate
def add_item(self, item, price, tva=True):
self.items.append((item, float(price), tva))
def calculate_total(self) -> (float, float, float):
values = [0, 0, 0]
for item, price, tva in self.items:
values[0] += price / (1 + self.tva_rate) if tva else price
values[1] += round((price / (1 + self.tva_rate)) * self.tva_rate, 2) if tva else 0
values[1] = int(values[1] *100) / 100
values[2] += price
return tuple(values)
if __name__ == '__main__':
doctest.testmod()
bng = BillNumberGenerator()
print(bng.generate_number(date(2020, 1, 1)))
print(bng.generate_number(date(2020, 1, 1)))
bng.set_next_counter_for_year(2020, 38)
print(bng.generate_number(date(2020, 1, 1)))
The code is still in prototype mode, so please be gentle regarding inefficient stuff :)