6
\$\begingroup\$

Overview

I've just written a package in Python (which I've named sibstructures) whose purpose is to assist staff in managing a coldstore, which is a warehouse-sized refrigerator holding (in this case) boxes of potatoes.

The code below is one file taken from that package, the file which defines the centrepiece Coldstore class. It also defines a number of "helper" classes, i.e. small classes whose sole purpose is to facilitate something or other in Coldstore. The file then finishes with some unit tests.

I'm pretty much self-taught Python, so any remarks would be greatly appreciated, but I'd be particularly grateful for any guidance - or praise - regarding the way in which I've structured the file.

The Code

Opening Lines

This follows the proforma by which I begin almost all of my .py files.

### This code defines a class, which models a potato coldstore.

# Imports.
import time

# Custom imports.
from sibstructures import sibconstants
from sibstructures import data_storage
from sibstructures import layer_colours
from sibstructures.box import Box
from sibstructures.spot import Spot
from sibstructures.numerals import to_roman, to_arabic

# Local constants.
max_tickets = 100

Main Class

There then follows the definition of the "main" class, which in this case models the coldstore itself.

A few features are worth explaining:

  • The data_storage module allows access to a database, which (1) records what enters and exits the coldstore, (2) records where boxes are placed, and (3) stores plans for where boxes should be stored.
  • Scanners (using RFID) identify boxes as they enter and leave.
  • Using these scanners, the coldstore issues a recommendation to the storesman as to where to put a given box as it enters.
  • Upon receiving a recommendation, the storesman may either (1) follow the recommendation, (2) cancel the recommendation, or (3) override the recommendation (i.e. put the box somewhere else).
##############
# MAIN CLASS #
##############

# The class in question.
class Coldstore:
  def __init__(self, code):
    self.code = code
    self.columns = None
    self.headlands = None
    self.ticket_machine = Ticket_Machine()
    self.outstanding_recommendations = []

  # Load a map of this coldstore (when empty) from plain text.
  def load_floorplan_from_plain_text(self, plain_text):
    lines = plain_text.split("\n")
    no_of_columns = len(lines[0].split(" "))
    sheet_columns = []
    for i in range(no_of_columns):
      max_layers_list = []
      for line in lines:
        max_layers = line.split(" ")
        max_layers_list.append(int(max_layers[i]))
      max_layers_tuple = tuple(max_layers_list)
      sheet_columns.append(max_layers_tuple)
    result = []
    i = 0
    for item in sheet_columns:
      letter = chr(ord('a')+i)
      column = Column(letter, item)
      result.append(column)
    self.columns = result

  # Load the headlands of this coldstore from two integers.
  def load_headlands_from_arguments(self, no_of_spots, max_layers):
    result = []
    for i in range(1, no_of_spots+1):
      numeral = to_roman(i)
      spot = Spot(numeral, max_layers)
      result.append(spot)
    self.headlands = result

  # Load a coldstore's floorplan from the database.
  def load_floorplan_from_db(self):
    data = data_storage.fetch_local_specifics()
    if data == None:
      raise Exception("No local specifics to load.")
    else:
      floorplan = data["floorplan"]
      headlands_spots = data["headlands_spots"]
      headlands_maxlayers = data["headlands_maxlayers"]
      self.load_floorplan_from_plain_text(floorplan)
      self.load_headlands_from_arguments(headlands_spots,
                                         headlands_maxlayers)

  # Ronseal.
  def select_column_by_letter(self, letter):
    for column in self.columns:
      if column.letter == letter:
        return column
    raise Exception("No column with letter="+letter+".")

  # Executes the delivery of a box to a given column, and logs the same.
  def execute_drop(self, epc, column_letter, row_numeral):
    box = Box(epc, None)
    column = self.select_column_by_letter(column_letter)
    spot = column.select_spot_by_row(row_numeral)
    if spot.add_one(box):
      data_storage.commit_drop(epc, column_letter, spot.row_id,
                               int(time.time()))
    else:
      raise Exception("It should be impossible to add a box at column="+
                      column_letter+", row="+row_numeral+".")

  # Take action upon detecting a given EPC entering this coldstore.
  def execute_entry(self, epc, timestamp):
    data_storage.add_entry_log(epc, timestamp)
    self.issue_recommendation(epc)

  # Take action upon detecting a given EPC leaving this coldstore.
  def execute_exit(self, epc, timestamp):
    data_storage.add_exit_log(epc, timestamp)
    result = self.remove_box_by_epc(epc)
    return result

  # Add a recommendation to the list, given that a specific EPC was detected
  # entering this coldstore.
  def issue_recommendation(self, epc):
    ticket = self.ticket_machine.issue_ticket()
    permitted_columns = data_storage.fetch_permitted_columns(epc)
    for letter in permitted_columns:
      column = self.select_column_by_letter(letter)
      for spot in column.spots:
        if spot.has_room():
          column_letter = letter
          row_numeral = spot.row_id
          recommendation = Recommendation(ticket, epc, column_letter,
                                          row_numeral)
          self.outstanding_recommendations.append(recommendation)
          return
    column_letter = None
    row_numeral = None
    recommendation = Recommendation(ticket, epc, column_letter, row_numeral)
    self.outstanding_recommendations.append(recommendation)

  # Remove a box with a given EPC from this coldstore, if possible.
  def remove_box_by_epc(self, epc):
    for column in self.columns:
      for spot in column.spots:
        if spot.remove_one(epc):
          return True
    return False

  # Select a recommendation by its ticket.
  def select_recommendation_by_ticket(self, ticket):
    for rec in self.outstanding_recommendations:
      if rec.ticket == ticket:
        return rec
    return None

  # Follow up on an outstanding recommendation.
  def follow_recommendation(self, ticket):
    rec = self.select_recommendation_by_ticket(ticket)
    if rec == None:
      raise Exception("No outstanding recommendation with ticket="+
                      str(ticket)+".")
    elif rec.column_letter == None:
      raise Exception("Cannot follow a void recommendation.")
    else:
      self.execute_drop(rec.epc, rec.column_letter, rec.row_numeral)
      self.outstanding_recommendations.remove(rec)
      self.ticket_machine.return_ticket(ticket)

  # Cancel a given outstanding recommendation.
  def cancel_recommendation(self, ticket):
    rec = self.select_recommendation_by_ticket(ticket)
    self.outstanding_recommendations.remove(rec)
    self.ticket_machine.return_ticket(ticket)

  # Cancel a given outstanding recommendation, and do something else with
  # that box.
  def override_recommendation(self, ticket, column_letter, row_numeral):
    rec = self.select_recommendation_by_ticket(ticket)
    epc = rec.epc
    self.cancel_recommendation(ticket)
    self.execute_drop(epc, column_letter, row_numeral)
    self.ticket_machine.return_ticket(ticket)

  # Ronseal.
  def print_me(self):
    printer = Coldstore_Printer(self)
    printer.print_me()

Helper Classes

A chocolate box of small, mostly self-explanatory classes.

################################
# HELPER CLASSES AND FUNCTIONS #
################################

# A class which models a single column of a coldstore.
class Column:
  def __init__(self, letter, max_layers_tuple):
    self.letter = letter
    self.spots = self.make_spots(max_layers_tuple)

  # Add the column's spots, from a tuple of their max_layers.
  def make_spots(self, max_layers_tuple):
    i = 1
    result = []
    for max_layers in max_layers_tuple:
      numeral = to_roman(i)
      spot = Spot(numeral, max_layers)
      result.append(spot)
      i = i+1
    return result

  # Ronseal.
  def select_spot_by_row(self, row_id):
    for spot in self.spots:
      if spot.row_id == row_id:
        return spot
    raise Exception("No spot with row_id="+row_id+".")

# A class which issues and recycles tickets, used to keep track of any
# recommendations issued.
class Ticket_Machine:
  def __init__(self):
    self.tickets = set()
    for i in range(max_tickets):
      ticket = i+1
      self.tickets.add(ticket)

  # Ronseal.
  def issue_ticket(self):
    result = self.tickets.pop()
    return result

  # Ronseal.
  def return_ticket(self, ticket):
    self.tickets.add(ticket)

# A bare-bones class to hold the properties of a recommmendation.
class Recommendation:
  def __init__(self, ticket, epc, column_letter, row_numeral):
    self.ticket = ticket
    self.epc = epc
    self.column_letter = column_letter
    self.row_numeral = row_numeral

# A class which permits the printing of a coldstore to the screen.
class Coldstore_Printer:
  def __init__(self, coldstore):
    self.coldstore = coldstore
    self.rows = self.make_rows()

  # Rearrange the coldstore's columns into rows, which can be printed more
  # easily, and extract the useful data from each spot.
  def make_rows(self):
    result = []
    for i in range(len(self.coldstore.columns[0].spots)):
      row = []
      for column in self.coldstore.columns:
        printed_spot = Printed_Spot(column.spots[i])
        row.append(printed_spot)
      result.append(row)
    return result

  # Makes the top row of the printout.
  def get_top_row(self):
    result = " "
    for i in range(len(self.rows[0])):
      result = result+"  "+chr(ord('a')+i)+"   "
    return result

  # Makes the tween rows of the printout.
  def get_tween_row(self):
    result = "+"
    for _ in range(len(self.rows[0])):
      result = result+"-----+"
    return result

  # Ronseal.
  def print_me(self):
    result = []
    top_row = self.get_top_row()
    tween_row = self.get_tween_row()
    i = 1
    result.append(top_row)
    result.append(tween_row)
    for row in self.rows:
      printout_for_row = "|"
      for spot in row:
        printout_for_row = printout_for_row+" "+spot.get_printout()+" |"
      printout_for_row = printout_for_row+" "+to_roman(i)
      result.append(printout_for_row)
      result.append(tween_row)
      i = i+1
    for item in result:
      print(item)

# A class which is a component of the Coldstore_Printer class.
class Printed_Spot:
  LAYER_COLOURS = (None, layer_colours.RED, layer_colours.ORANGE,
                   layer_colours.YELLOW, layer_colours.GREEN,
                   layer_colours.BLUE, layer_colours.INDIGO,
                   layer_colours.VIOLET)
  MAXED_OUT_LAYER_COLOUR = layer_colours.VIOLET
  RESET = layer_colours.RESET
  def __init__(self, spot):
    self.layers = spot.layers()
    if spot.max_layers == 0:
      self.is_void = True
    else:
      self.is_void = False
    if self.layers == 0:
      self.is_empty = True
    else:
      self.is_empty = False
    if (self.is_void or self.is_empty):
      self.top_display_code = None
    else:
      self.top_display_code = spot.boxes[-1].display_code

  # Ronseal.
  def get_printout(self):
    if self.is_void:
      result = "XXX"
    elif self.is_empty:
      result = "   "
    else:
      colour = Printed_Spot.LAYER_COLOURS[self.layers]
      result = colour+self.top_display_code+Printed_Spot.RESET
    return result

Testing

Finally, there's some unit-testing. It's mostly unambitious: I try to make sure that the classes do what they're supposed to do, and don't try too hard to break them.

###########
# TESTING #
###########

# Test the Column class.
def test_column():
  column = Column('a', (0, 2, 3))
  assert(column.letter == 'a')
  assert(column.spots[0].max_layers == 0)
  assert(column.spots[0].row_id == "i")
  assert(column.spots[1].max_layers == 2)
  assert(column.spots[1].row_id == "ii")
  assert(column.spots[2].max_layers == 3)
  assert(column.spots[2].row_id == "iii")
  assert(len(column.spots[2].boxes) == 0)

# Test the Coldstore class.
def test_coldstore():
  test_floorplans()
  # These tests depend on agreeable data in the DB.
  test_drops()
  test_entry_and_exit()
  test_recommendations()
  #test_printing()

# Test the loading of floorplans by various means.
def test_floorplans():
  plain_text = ("1 2 3\n"+
                "4 5 6\n"+
                "7 8 9")
  coldstore = Coldstore("cs0")
  coldstore.load_floorplan_from_plain_text(plain_text)
  assert(len(coldstore.columns) == 3)
  column_a = coldstore.columns[0]
  assert(column_a.letter == 'a')
  assert(len(column_a.spots) == 3)
  spot_i = column_a.spots[0]
  assert(spot_i.row_id == "i")
  assert(spot_i.max_layers == 1)
  spot_iii = column_a.spots[2]
  assert(spot_iii.row_id == "iii")
  assert(spot_iii.max_layers == 7)
  # The remaining tests depend on agreeable data in the DB.
  coldstore.load_floorplan_from_db()
  assert(len(coldstore.columns) == 3)
  column_a = coldstore.columns[0]
  assert(column_a.letter == 'a')
  assert(len(column_a.spots) == 3)
  spot_i = column_a.spots[0]
  assert(spot_i.row_id == "i")
  assert(spot_i.max_layers == 1)
  spot_iii = column_a.spots[2]
  assert(spot_iii.row_id == "iii")
  assert(spot_iii.max_layers == 7)
  assert(len(coldstore.headlands) == 3)
  assert(coldstore.headlands[0].max_layers == 7)
  assert(coldstore.headlands[2].max_layers == 7)

# Test dropping boxes. Depends on agreeable data in DB.
def test_drops():
  coldstore = Coldstore("cs0")
  coldstore.load_floorplan_from_db()
  coldstore.execute_drop("epc0", 'a', "i")
  # Now check database.

# Test the entry and exit routines. Depends on agreeable data in DB.
def test_entry_and_exit():
  coldstore = Coldstore("cs0")
  coldstore.load_floorplan_from_db()
  coldstore.execute_entry("epc0", int(time.time()))
  assert(coldstore.execute_exit("epc0", int(time.time())) == False)
  rec = coldstore.outstanding_recommendations[0]
  assert(rec.epc == "epc0")
  assert(rec.column_letter == 'a')
  assert(rec.row_numeral == "i")

# Test the recommendation system. Depends on agreeable data in DB.
def test_recommendations():
  coldstore = Coldstore("cs0")
  coldstore.load_floorplan_from_db()
  coldstore.execute_entry("epc0", int(time.time()))
  assert(len(coldstore.ticket_machine.tickets) == max_tickets-1)
  coldstore.follow_recommendation(
    coldstore.outstanding_recommendations[0].ticket)
  assert(coldstore.execute_exit("epc0", int(time.time())) == True)
  assert(len(coldstore.ticket_machine.tickets) == max_tickets)
  coldstore.execute_entry("epc0", int(time.time()))
  coldstore.cancel_recommendation(
    coldstore.outstanding_recommendations[0].ticket)
  assert(len(coldstore.outstanding_recommendations) == 0)
  coldstore.execute_entry("epc0", int(time.time()))
  coldstore.override_recommendation(
    coldstore.outstanding_recommendations[0].ticket, 'a', "i")
  assert(len(coldstore.outstanding_recommendations) == 0)
  assert(coldstore.execute_exit("epc0", int(time.time())) == True)

# A sub-function for "test_printing()".
def introduce_epc(coldstore, epc):
  if len(coldstore.outstanding_recommendations) != 0:
    raise Exception("Pre-existing outstanding recommendations.")
  coldstore.execute_entry(epc, int(time.time()))
  coldstore.follow_recommendation(
    coldstore.outstanding_recommendations[0].ticket)

# Test the facilities used to print coldstores to the screen.
def test_printing():
  coldstore = Coldstore("cs0")
  coldstore.load_floorplan_from_db()
  introduce_epc(coldstore, "epc0")
  introduce_epc(coldstore, "epc1")
  introduce_epc(coldstore, "epc2")
  introduce_epc(coldstore, "epc3")
  introduce_epc(coldstore, "epc4")
  introduce_epc(coldstore, "epc5")
  coldstore.print_me()

# Run the unit tests.
def test():
  test_column()
  test_coldstore()
  print("Tests passed!")

Wrapping Up

I always put this at the end of my .py files, so I thought I'd check I've been doing the right thing!

###################
# RUN AND WRAP UP #
###################

def run():
  test()

if __name__ == "__main__":
  run()

Other Stuff

A "review copy" of the whole package is available at https://github.com/tomhosker/sibstructures4review.

\$\endgroup\$
  • \$\begingroup\$ Are all of these one file? Whether or not they are could you provide the file structure that you're using, and highlight which file each code block is in. Currently I'm confused on how to copy this package and start working on it. In addition do you use something like python sibstructures, cd sibstructures; python main.py, python -m sibstructures or pytest to run your tests? \$\endgroup\$ – Peilonrayz Jan 22 at 18:41
  • \$\begingroup\$ @Peilonrayz Yes, all the code quoted in this question is from one file. Indeed, if you were to put all the snippets above into one file, in the order in which they appear above, you would have the whole file. \$\endgroup\$ – Tom Hosker Jan 22 at 22:49
  • \$\begingroup\$ @Peilonrayz I need to use the structures I created in sibstructures in multiple, disparate places in the repository I'm working on, and so I concluded that the best solution would be to make sibstructures into a custom package by copying it into ~/.local/lib/python3.6 - that path being specific to Linux. (If you know of a better solution, I'd very much like to learn about it.) I can then run this file - named coldstore.py - by navigating into the appropriate directory and running python3 coldstore.py, and that's it. \$\endgroup\$ – Tom Hosker Jan 22 at 22:58
  • \$\begingroup\$ @Peilonrayz My employer has asked me to work on this project in a private repository. However, since you've flattered me by taking an interest, and since it can't do any harm to share just this utilities package, I'll put it on GitHub so you can look at it properly. That said, I'm just about to go to bed, and so I'll do it in the morning (9am UK time). Hope you can wait till then! \$\endgroup\$ – Tom Hosker Jan 22 at 23:03
  • \$\begingroup\$ @Peilonrayz Okay, I've made the GitHub repository: github.com/tomhosker/sibstructures4review. \$\endgroup\$ – Tom Hosker Jan 23 at 9:17
3
\$\begingroup\$

Project Structure

  • I need to use the structures I created in sibstructures in multiple, disparate places in the repository I'm working on, and so I concluded that the best solution would be to make sibstructures into a custom package by copying it into ~/.local/lib/python3.6 - that path being specific to Linux. (If you know of a better solution, I'd very much like to learn about it.)

    This is good, you've correctly setup a Python package so that it runs correctly. You however have manually deployed the package. Now there's the motto "if it ain't broke, don't fix it", and I feel that can come into play here. If you're happy with this as a solution, you do you.

    However if you may want to create a setuptools package to be able to deploy your script easier.

    • Solo: If you're a solo developer, then you won't see too many benefits from utilizing setuptools packages in their intended way. It does however allow you to use the following commands to install your package.

      $ python -m pip install /path/to/sibstructures
      
    • Team: Given how basic Python's packaging hosting infrastructure is, you can host your own private PyPI repository. And with a small change to your pip config changing index-url to your host. Then you can install using pip easily.

      $ python -m pip install sibstructures
      

    So again, if you're happy with what you have, you do you. But you may find changing to how the Python eco-system works easier. After the initial setup.

  • You're method of testing isn't really standard. There's nothing wrong with this, but using tools like pytest or unittest give nicer output, and allow greater separation of your project.

    Since all of your tests start with test_ and you're using assert to test your code, migrating to pytest would be a doddle.

    I have previously written two answers highlighting how to utilize pytest. The first is a very basic method, where the second utilizes tox. I would highly recommend reading both, and utilizing tox where possible.

I'm not bashing on you rolling your own package management or testing. The way you've done it is pretty decent. However I know I benefit from the above tools, and so I'm sure they will help you too.

Code Review

  • I suggest installing a linter, like flake8 or Prospector. Your code is almost very Pythonic, but there are some minor aspects that are preventing you from being there.

    You may like a hinter, like black, to automatically change your code to be more compliant. However I would recommend installing a linter even if you have a hinter as a hinter can't fix everything.

  • With Python it's an industry standard to indent to 4 spaces. In all my time here it has been very rare for me to not see 4 spaces being used.
  • Your comments before the classes or functions would be better as docstrings. This is because tools like Sphinx can read them and convert them into documentation.

    Most programs in the Python ecosystem use Sphinx. Most of the above links to the different packages are websites that Sphinx created.

  • For load_floorplan_from_plain_text:

    • The function is fairly cluttered. you don't need max_layers_tuple when you could just use sheet_columns.append(tuple(max_layers)) which is one character longer. This is a hindrance to the readability of your code.
    • I have a personal distaste for var_list and var_tuple. If you have to specify the type then you're doing something wrong.
    • After i = 0, i never changes. This means all your Columns have the same letter.
    • You can better describe the function as three comprehensions.

    def load_floorplan_from_plain_text(self, plain_text):
        """Load a map of this coldstore (when empty) from plain text."""
        lines = plain_text.split("\n")
        no_of_columns = len(lines[0].split(" "))
        sheet_columns = [
            [
                int(line.split(" ")[i])
                for line in lines
            ]
            for i in range(no_of_columns)
        ]
        self.columns = [
            Column('a', item)
            for item in sheet_columns
        ]
    
    • If you split plain_text so that it is a 2d array by default then you can simplify the creation of the columns.
    • You can also merge the comprehensions for sheet_columns and self.columns as the content is the same.
  • Since the function names in load_floorplan_from_db are rather long I would just enter the arguments on the next lines, rather than having people read the text on the other side of the monitor.

  • In execute_drop I would use a guard to error so the happy path is always the outermost level it can be.
  • remove_box_by_epc could be better described as a comprehension and any.
  • Always use is when comparing to None.
  • You can use enumerate to get the current index and the item.
  • You can use f-strings or str.format to format strings. Rather than using + and str manually.

    >>> foo = 'foo'
    >>> f'{foo} bar'
    'foo bar'
    >>> f'{} bar'.format(foo)
    'foo bar'
    
  • Your class Coldstore_Printer should be a static class where you only have one public method print.

    >>> Coldstore_Printer.print(coldstore)
    ...
    
  • You should merge Printed_Spot into Coldstore_Printer as it can be defined in less than ten lines in a function.

  • The spots value of the Column class can just be a dictionary. Since you only want to look up by the spot id.
  • You could make a class or function to more easily build spots.
  • You should only instantiate Coldstore with all the data it needs. Delayed initialization is janky and can lead to some really crippling errors. To ensure your function is correctly setup you can utilize classmethods to run before __init__. Now to create the class you use:

    coldstore = Coldstore.from_db(code, data_storage.fetch_local_specifics())
    
  • I think your code has some more problems, as I don't think you're using datatypes or classes in the best way. But I can't check and I've changed enough of your code.

Note: Not tested, and only provided as an example.

### This code defines a class, which models a potato coldstore.

# Imports.
import time

# Custom imports.
from sibstructures import sibconstants
from sibstructures import data_storage
from sibstructures import layer_colours
from sibstructures.box import Box
from sibstructures.spot import Spot
from sibstructures.numerals import to_roman, to_arabic

# Local constants.
max_tickets = 100


class Coldstore:
    def __init__(self, code, columns, headlands):
        self.code = code
        self.columns = {column.letter: column for column in columns}
        self.headlands = headlands
        self.ticket_machine = Ticket_Machine()
        self.outstanding = {}
        self.outstanding_recommendations

    @classmethod
    def _load_floorplan(cls, plain_text):
        """Load a map of this coldstore (when empty) from plain text."""
        data = [
            row.split(" ")
            for row in plain_text.split("\n")
        ]
        return [
            Column(
                'a',
                [int(row[i]) for row in data]
            )
            for i in range(len(data[0]))
        ]

    @classmethod
    def _load_headlands(cls, no_of_spots, max_layers):
        """Load the headlands of this coldstore from two integers."""
        return spots_from_layers([max_layers] * no_of_spots)

    @classmethod
    def from_db(cls, code, data):
        """Load a coldstore's floorplan from the database."""
        if data is None:
            raise Exception("No local specifics to load.")

        return cls(
            code,
            columns=cls._load_floorplan(
                data["floorplan"],
            ),
            headlands=cls.load_headlands_from_arguments(
                data["headlands_spots"],
                data["headlands_maxlayers"],
            ),
        )

    def execute_drop(self, epc, column_letter, row_numeral):
        """Executes the delivery of a box to a given column, and logs the same."""
        box = Box(epc, None)
        column = self.columns[column_letter]
        spot = column.spots[row_numeral]
        if not spot.add_one(box):
            raise Exception(
                f"It should be impossible to add a box at "
                f"column={column_letter}"
                f", row={row_numeral}."
            )
        data_storage.commit_drop(
            epc,
            column_letter,
            spot.row_id,
            int(time.time()),
        )

    def execute_entry(self, epc, timestamp):
        """Take action upon detecting a given EPC entering this coldstore."""
        data_storage.add_entry_log(epc, timestamp)
        self.issue_recommendation(epc)

    def execute_exit(self, epc, timestamp):
        """Take action upon detecting a given EPC leaving this coldstore."""
        data_storage.add_exit_log(epc, timestamp)
        return self.remove_box_by_epc(epc)

    def issue_recommendation(self, epc):
        """
        Add a recommendation to the list, given that a specific EPC was detected
        entering this coldstore.
        """
        rec = self._issue_recommendation(epc)
        self.outstanding[rec.ticket] = rec

    def _issue_recommendation(self, epc):
        ticket = self.ticket_machine.issue_ticket()
        for letter in data_storage.fetch_permitted_columns(epc):
            column = self.columns[letter]
            for spot in column.spots.values():
                if spot.has_room():
                    return Recommendation(
                        ticket,
                        epc,
                        letter,
                        spot.row_id,
                    )
        return Recommendation(
            ticket,
            epc,
            None,
            None,
        )

    def remove_box_by_epc(self, epc):
        """Remove a box with a given EPC from this coldstore, if possible."""
        return any(
            spot.remove_one(epc)
            for column in self.columns.values()
            for spot in column.spots.values()
        )

    def follow_recommendation(self, ticket):
        """Follow up on an outstanding recommendation."""
        rec = self.outstanding[ticket]
        if rec.column_letter is None:
            raise Exception("Cannot follow a void recommendation.")

        self.execute_drop(rec.epc, rec.column_letter, rec.row_numeral)
        self.cancel_recommendation(ticket)

    def cancel_recommendation(self, ticket):
        """Cancel a given outstanding recommendation."""
        self.outstanding.pop(ticket)
        self.ticket_machine.return_ticket(ticket)

    def override_recommendation(self, ticket, column_letter, row_numeral):
        """
        Cancel a given outstanding recommendation, and do something else with
        that box.
        """
        rec = self.outstanding[ticket]
        epc = rec.epc
        self.execute_drop(epc, column_letter, row_numeral)
        self.cancel_recommendation(ticket)
        self.ticket_machine.return_ticket(ticket)

    def print_me(self):
        """Ronseal."""
        Printer.print(self)


################################
# HELPER CLASSES AND FUNCTIONS #
################################


def spots_from_layers(layers):
    spots = (
        Spot(to_roman(i), layer)
        for i, layer in enumerate(layers, 1)
    )
    return {
        spot.row_id: spot
        for spot in spots
    }


class Column:
    """A class which models a single column of a coldstore."""
    def __init__(self, letter, layers):
        self.letter = letter
        self.spots = spots_from_layers(layers)


class Ticket_Machine:
    """
    A class which issues and recycles tickets, used to keep track of any
    recommendations issued.
    """
    def __init__(self):
        self.tickets = set(range(1, max_tickets+1))

    def issue_ticket(self):
        """Ronseal."""
        return self.tickets.pop()

    def return_ticket(self, ticket):
        """Ronseal."""
        self.tickets.add(ticket)


class Recommendation:
    """A bare-bones class to hold the properties of a recommmendation."""
    def __init__(self, ticket, epc, column_letter, row_numeral):
        self.ticket = ticket
        self.epc = epc
        self.column_letter = column_letter
        self.row_numeral = row_numeral


class Printer:
    """Static class to print a ColdStore."""
    LAYER_COLOURS = (
        None,
        layer_colours.RED,
        layer_colours.ORANGE,
        layer_colours.YELLOW,
        layer_colours.GREEN,
        layer_colours.BLUE,
        layer_colours.INDIGO,
        layer_colours.VIOLET,
    )
    MAXED_OUT_LAYER_COLOUR = layer_colours.VIOLET
    RESET = layer_colours.RESET

    @classmethod
    def print(cls, coldstore):
        print("\n".join(cls._print(coldstore)))

    @classmethod
    def _print(cls, coldstore):
        rows = self._make_rows(coldstore)
        yield self._get_top_row()
        TWEEN_ROW = self._get_tween_row()
        yield TWEEN_ROW
        for i, row in enumerate(rows):
            yield (
                "|"
                + "".join(
                    f" {cls._print_spot(spot)} |"
                    for spot in row
                )
                + " "
                + to_roman(i)
            )
            yield TWEEN_ROW

    @classmethod
    def _make_rows(cls, coldstore):
        """
        Rearrange the coldstore's columns into rows, which can be printed more
        easily, and extract the useful data from each spot.
        """
        return [
            [
              column.spots[i]
              for column in coldstore.columns
            ]
            for i in range(len(coldstore.columns[0].spots))
        ]

    @classmethod
    def _get_top_row(self, rows):
        """Makes the top row of the printout."""
        return ''.join(
            f"  {chr(ord('a')+i)}   "
            for i in range(len(rows[0]))
        )

    @classmethod
    def _get_tween_row(self, rows):
        """Makes the tween rows of the printout."""
        return "+" + ''.join(
            "-----+"
            for _ in range(len(rows[0]))
        )

    @classmethod
    def _print_spot(self, spot):
        if spot.max_layers == 0:
            return "XXX"
        if spot.layers == 0:
            return "   "
        return (
            cls.LAYER_COLOURS[spot.layers]
            + spot.boxes[-1].display_code
            + cls.RESET
        )
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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