# ============================================================================== # Copyright (C) 2019 - Philip Paquette # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # ============================================================================== """ Daide Clauses - Contains clauses that can be used to build / parse requests and responses """ from abc import ABCMeta, abstractmethod import logging from diplomacy.daide import tokens from diplomacy.daide.tokens import Token # Constants LOGGER = logging.getLogger(__name__) def break_next_group(daide_bytes): """ If the next token is a parenthesis, finds its matching closing parenthesis and returns a tuple of the items between parentheses and the items after the closing parenthesis. e.g. bytes for (ENG AMY PAR) MTO NWY would return --> (ENG AMY PAR) + MTO NWY e.g. bytes for ENG AMY PAR would return -> '' + ENG AMY PAR since the byte array does not start with a "(" :return: A tuple consisting of the parenthesis group and the remaining bytes after the group or an empty byte array and the entire byte array if the byte array does not start with a parenthesis """ if not daide_bytes: return b'', b'' # Finding the matching closing parenthesis pos = 0 parentheses_level = 0 while True: if daide_bytes[pos:pos + 2] == bytes(tokens.OPE_PAR): parentheses_level += 1 elif daide_bytes[pos:pos + 2] == bytes(tokens.CLO_PAR): parentheses_level -= 1 if parentheses_level <= 0: break if pos >= len(daide_bytes): # Parentheses don't match - Not returning group pos = 0 break pos += 2 # Returning return (daide_bytes[:pos + 2], daide_bytes[pos + 2:]) if pos else (None, daide_bytes) def add_parentheses(daide_bytes): """ Add parentheses to a list of bytes """ if not daide_bytes: return daide_bytes return bytes(tokens.OPE_PAR) + daide_bytes + bytes(tokens.CLO_PAR) def strip_parentheses(daide_bytes): """ Removes parentheses from the DAIDE bytes and returns the inner content. The first and last token are expected to be parentheses. """ assert daide_bytes[:2] == bytes(tokens.OPE_PAR), 'Expected bytes to start with "("' assert daide_bytes[-2:] == bytes(tokens.CLO_PAR), 'Expected bytes to end wth ")"' return daide_bytes[2:-2] def parse_bytes(clause_constructor, daide_bytes, on_error='raise'): """ Creates a clause object from a string of bytes :param clause_constructor: The type of clause to build :param daide_bytes: The bytes to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: A tuple of 1) the clause object, and 2) the remaining (unparsed) bytes """ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"' clause = clause_constructor() daide_bytes = clause.from_bytes(daide_bytes, on_error=on_error) if not clause.is_valid: return None, daide_bytes return clause, daide_bytes def parse_string(clause_constructor, string, on_error='raise'): """ Creates a clause object from a string :param clause_constructor: The type of clause to build :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The clause object """ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"' clause = clause_constructor() clause.from_string(string, on_error=on_error) if not clause.is_valid: return None return clause class AbstractClause(metaclass=ABCMeta): """ Abstract version of a DAIDE clause """ def __init__(self): """ Constructor """ self._is_valid = True @property def is_valid(self): """ Indicates if the clause is valid (no errors were triggered) """ return self._is_valid @abstractmethod def __bytes__(self): """ Define the DAIDE bytes representation """ raise NotImplementedError() @abstractmethod def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ raise NotImplementedError() @abstractmethod def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ raise NotImplementedError() def error(self, on_error, message=''): """ Performs the error action :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :param message: The message to display """ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"' if on_error == 'raise': raise RuntimeError(message) if on_error == 'warn': LOGGER.warning(message) self._is_valid = False class SingleToken(AbstractClause): """ Extracts a single token e.g. NME """ def __init__(self): """ Constructor """ super(SingleToken, self).__init__() self._bytes = b'' self._str = '' def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ token_bytes, remaining_bytes = daide_bytes[:2], daide_bytes[2:] # Not enough bytes to get a token if not token_bytes: self.error(on_error, 'At least 2 bytes are required to build a token.') return remaining_bytes # Getting the token self._bytes = token_bytes self._str = str(Token(from_bytes=token_bytes)) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ # Not enough bytes to get a token if not string: self.error(on_error, '`string` cannot be empty or None') return # Getting the token self._bytes = bytes(Token(from_str=string)) self._str = string class Power(SingleToken): """ Each clause is a power Syntax: ENG """ _alias_from_bytes = {'AUS': 'AUSTRIA', 'ENG': 'ENGLAND', 'FRA': 'FRANCE', 'GER': 'GERMANY', 'ITA': 'ITALY', 'RUS': 'RUSSIA', 'TUR': 'TURKEY'} _alias_from_string = {value: key for key, value in _alias_from_bytes.items()} def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ remaining_bytes = super(Power, self).from_bytes(daide_bytes, on_error) self._str = self._alias_from_bytes.get(self._str, self._str) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ str_power = self._alias_from_string.get(string, string) super(Power, self).from_string(str_power, on_error) class String(AbstractClause): """ A string contained between parentheses Syntax (Text) """ def __init__(self): """ Constructor """ super(String, self).__init__() self._bytes = b'' self._str = '' def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ str_group_bytes, remaining_bytes = break_next_group(daide_bytes) # Can't find the string if not str_group_bytes: self.error(on_error, 'Unable to find a set of parentheses to extract the string clause.') return daide_bytes # Extract its content nb_bytes = len(str_group_bytes) self._bytes = str_group_bytes self._str = ''.join([str(Token(from_bytes=str_group_bytes[pos:pos + 2])) for pos in range(2, nb_bytes - 2, 2)]) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ self._bytes = add_parentheses(b''.join([bytes(Token(from_str=char)) for char in string])) self._str = string class Number(AbstractClause): """ A number contained between parentheses Syntax: Number """ def __init__(self): """ Constructor """ super(Number, self).__init__() self._bytes = b'' self._int = 0 def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return str(self._int) def __int__(self): """ Return the Diplomacy int """ return self._int def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ if not daide_bytes: self.error(on_error, 'Expected at least 1 byte to parse a number') return daide_bytes number_bytes, remaining_bytes = daide_bytes[:2], daide_bytes[2:] number_token = Token(from_bytes=number_bytes) if not tokens.is_integer_token(number_token): self.error(on_error, 'The token is not an integer. Got %s' % number_token) return daide_bytes # Extract its content self._bytes = number_bytes self._int = int(number_token) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ self._bytes = bytes(Token(from_int=int(string))) self._int = int(string) class Province(AbstractClause): """ Each clause is an province token Syntax: ADR (STP ECS) """ _alias_from_bytes = {'ECS': '/EC', 'NCS': '/NC', 'SCS': '/SC', 'WCS': '/WC', 'ECH': 'ENG', 'GOB': 'BOT', 'GOL': 'LYO'} _alias_from_string = {value: key for key, value in _alias_from_bytes.items()} def __init__(self): """ Constructor """ super(Province, self).__init__() self._bytes = b'' self._str = '' def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ province_group_bytes, remaining_bytes = break_next_group(daide_bytes) # Is a province with coast # Syntax (STP NCS) if province_group_bytes: self._bytes = province_group_bytes province_group_bytes = strip_parentheses(province_group_bytes) province, province_group_bytes = parse_bytes(SingleToken, province_group_bytes, on_error=on_error) coast, province_group_bytes = parse_bytes(SingleToken, province_group_bytes, on_error=on_error) if province_group_bytes: self.error(on_error, '{} bytes remaining. Province is malformed'.format(len(province_group_bytes))) return daide_bytes str_province = self._alias_from_bytes.get(str(province), str(province)) str_coast = self._alias_from_bytes.get(str(coast), str(coast)) self._str = str_province + str_coast # Is a province with no coast # Syntax: ADR else: province, remaining_bytes = parse_bytes(SingleToken, remaining_bytes, on_error=on_error) self._bytes = bytes(province) self._str = self._alias_from_bytes.get(str(province), str(province)) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ province, coast = string.split('/') if '/' in string else [string, None] # Province with coast # Syntax: (STP NCS) if province and coast: str_province = self._alias_from_string.get(province, province) str_coast = self._alias_from_string.get('/' + coast, '') if not str_coast: self.error(on_error, 'Unknown coast "%s".' % '/' + coast) return self._str = str_province + str_coast self._bytes = add_parentheses(bytes(Token(from_str=str_province)) + bytes(Token(from_str=str_coast))) # Province without coast # Syntax: ADR else: str_province = self._alias_from_string.get(string, string) self._str = str_province self._bytes = bytes(Token(from_str=str_province)) class Turn(AbstractClause): """ Each clause is a Turn Syntax: (SPR 1901) """ _alias_from_bytes = {'AUT': 'F.R', 'FAL': 'F.M', 'SPR': 'S.M', 'SUM': 'S.R', 'WIN': 'W.A'} _alias_from_string = {value: key for key, value in _alias_from_bytes.items()} def __init__(self): """ Constructor """ super(Turn, self).__init__() self._bytes = b'' self._str = '' def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ turn_group_bytes, remaining_bytes = break_next_group(daide_bytes) # Can't find the order if not turn_group_bytes: self.error(on_error, 'Unable to find a set of parentheses to extract the turn clause.') return daide_bytes self._bytes = turn_group_bytes turn_group_bytes = strip_parentheses(turn_group_bytes) season, turn_group_bytes = parse_bytes(SingleToken, turn_group_bytes, on_error=on_error) year, turn_group_bytes = parse_bytes(Number, turn_group_bytes, on_error=on_error) if turn_group_bytes: self.error(on_error, '{} bytes remaining. Turn is malformed'.format(len(turn_group_bytes))) return daide_bytes season_alias = self._alias_from_bytes.get(str(season), str(season)) self._str = ''.join([season_alias[0], str(year), season_alias[-1]]) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ str_season = self._alias_from_string.get('%s.%s' % (string[0], string[-1]), '') str_year = string[1:-1] if not str_season or not str_year: self.error(on_error, 'Unknown season and/or year "%s".' % string) return self._str = string self._bytes = add_parentheses(bytes(Token(from_str=str_season)) + bytes(Token(from_int=int(str_year)))) class UnitType(SingleToken): """ Each clause is an season token Syntax: AMY """ _alias_from_bytes = {'AMY': 'A', 'FLT': 'F'} _alias_from_string = {value: key for key, value in _alias_from_bytes.items()} def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ remaining_bytes = super(UnitType, self).from_bytes(daide_bytes, on_error) self._str = self._alias_from_bytes.get(self._str, self._str) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ str_unit_type = self._alias_from_string.get(string, '') if not str_unit_type: self.error(on_error, 'Unknown unit type "%s"' % string) self._str = string self._bytes = bytes(Token(from_str=str_unit_type)) class Unit(AbstractClause): """ Each clause is an army or fleet Syntax: (ITA AMY TUN) """ _UNK = 'UNO' # Unknown power def __init__(self): """ Constructor """ super(Unit, self).__init__() self._bytes = b'' self._str = '' self._power_name = None @property def power_name(self): """ The power name """ return self._power_name def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ unit_group_bytes, remaining_bytes = break_next_group(daide_bytes) # Can't find the order if not unit_group_bytes: self.error(on_error, 'Unable to find a set of parentheses to extract the order clause.') return daide_bytes # Extract its content self._bytes = unit_group_bytes unit_group_bytes = strip_parentheses(unit_group_bytes) power, unit_group_bytes = parse_bytes(Power, unit_group_bytes, on_error=on_error) unit_type, unit_group_bytes = parse_bytes(UnitType, unit_group_bytes, on_error=on_error) province, unit_group_bytes = parse_bytes(Province, unit_group_bytes, on_error=on_error) if unit_group_bytes: self.error(on_error, '{} bytes remaining. Order is malformed'.format(len(unit_group_bytes))) return daide_bytes self._power_name = str(power) self._str = ' '.join([str(unit_type), str(province)]) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ words = string.split() # Checking number of words available if len(words) == 2: str_power = self._UNK str_unit_type, str_province = words elif len(words) == 3: str_power, str_unit_type, str_province = words else: self.error(on_error, 'Expected 2 or 3 words (e.g. "A PAR" or "FRANCE A PAR").') return # Parsing power = parse_string(Power, str_power, on_error=on_error) unit_type = parse_string(UnitType, str_unit_type, on_error=on_error) province = parse_string(Province, str_province, on_error=on_error) self._power_name = str(power) self._str = ' '.join([str(unit_type), str(province)]) self._bytes = add_parentheses(bytes(power) + bytes(unit_type) + bytes(province)) class OrderType(SingleToken): """ Each clause is an order token Syntax: SUB """ _alias_from_bytes = {'HLD': 'H', 'MTO': '-', 'SUP': 'S', 'CVY': 'C', 'CTO': '-', 'VIA': 'VIA', 'RTO': 'R', 'DSB': 'D', 'BLD': 'B', 'REM': 'D', 'WVE': 'WAIVE'} _alias_from_string = {'H': 'HLD', '-': 'MTO', 'S': 'SUP', 'C': 'CVY', 'VIA': 'VIA', 'R': 'RTO', 'D': 'REM', 'B': 'BLD', 'WAIVE': 'WVE'} def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ remaining_bytes = super(OrderType, self).from_bytes(daide_bytes, on_error) self._str = self._alias_from_bytes.get(self._str, self._str) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ str_order_type = self._alias_from_string.get(string, string) super(OrderType, self).from_string(str_order_type, on_error) def parse_order_to_bytes(phase_type, order_split): """ Builds an order clause from a byte array :param phase_type: The game phase :param order_split: An instance of diplomacy.utils.subject_split.OrderSplit :return: The order clause's bytes """ buffer = [] # FRANCE WAIVE if len(order_split) == 1: words = order_split.order_type.split() buffer.append(parse_string(Power, words.pop(0))) buffer.append(parse_string(OrderType, words.pop(0))) else: buffer.append(parse_string(Unit, order_split.unit)) # FRANCE F IRI [-] MAO # FRANCE A IRI [-] MAO VIA if order_split.order_type == '-': # FRANCE A IRI - MAO VIA if order_split.via_flag: buffer.append(Token(tokens.CTO)) else: buffer.append(Token(tokens.MTO)) # FRANCE A IRO [D] elif order_split.order_type == 'D': if phase_type == 'R': buffer.append(Token(tokens.DSB)) elif phase_type == 'A': buffer.append(Token(tokens.REM)) # FRANCE A LON [H] # FRANCE A WAL [S] FRANCE F LON # FRANCE A WAL [S] FRANCE F MAO - IRI # FRANCE F NWG [C] FRANCE A NWY - EDI # FRANCE A IRO [R] MAO # FRANCE A LON [B] # FRANCE F LIV [B] else: buffer.append(parse_string(OrderType, order_split.order_type)) # FRANCE A WAL S [FRANCE F LON] # FRANCE A WAL S [FRANCE F MAO] - IRI # FRANCE F NWG C [FRANCE A NWY] - EDI if order_split.supported_unit: buffer.append(parse_string(Unit, order_split.supported_unit)) # FRANCE A WAL S FRANCE F MAO [- IRI] # FRANCE F NWG C FRANCE A NWY [- EDI] if order_split.support_order_type: # FRANCE A WAL S FRANCE F MAO - IRI if order_split.order_type == 'S': buffer.append(Token(tokens.MTO)) buffer.append(parse_string(Province, order_split.destination[:3])) else: buffer.append(Token(tokens.CTO)) buffer.append(parse_string(Province, order_split.destination)) # FRANCE F IRI - [MAO] # FRANCE A IRI - [MAO] VIA # FRANCE A IRO R [MAO] elif order_split.destination: buffer.append(parse_string(Province, order_split.destination)) # FRANCE A IRI - MAO [VIA] if order_split.via_flag: buffer.append(parse_string(OrderType, order_split.via_flag)) return b''.join([bytes(clause) for clause in buffer]) class Order(AbstractClause): """ Each clause is an order Syntax: ((power unit_type location) order_type province) """ def __init__(self): """ Constructor """ super(Order, self).__init__() self._bytes = b'' self._str = '' self._power_name = None @property def power_name(self): """ The power name """ return self._power_name def __bytes__(self): """ Define the DAIDE bytes representation """ return self._bytes def __str__(self): """ Return the Diplomacy str """ return self._str def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes """ order_group_bytes, remaining_bytes = break_next_group(daide_bytes) # Can't find the order if not order_group_bytes: self.error(on_error, 'Unable to find a set of parentheses to extract the order clause.') return daide_bytes # Extract its content self._bytes = order_group_bytes # Parsing the unit group (or just the power) order_group_bytes = strip_parentheses(order_group_bytes) unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error='ignore') power = None if not unit: power, order_group_bytes = parse_bytes(Power, order_group_bytes, on_error='ignore') order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error=on_error) order_type_str = str(order_type) if order_type_str == 'WAIVE': str_buffer = [order_type_str] elif order_type_str: # Hold, Disband str_buffer = [str(unit), order_type_str] # Move order if order_type_str == '-': province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error) str_buffer += [str(province)] second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error='ignore') if str(second_order_type) == 'VIA': str_buffer += [str(second_order_type)] province_list, order_group_bytes = break_next_group(order_group_bytes) del province_list # Support elif order_type_str == 'S': other_unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error=on_error) str_buffer += [str(other_unit)] second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error='ignore') if str(second_order_type) == '-': province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error) str_buffer += [str(second_order_type), str(province)] # Convoy elif order_type_str == 'C': other_unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error=on_error) second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error=on_error) province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error) str_buffer += [str(other_unit), str(second_order_type), str(province)] # Retreat elif order_type_str == 'R': province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error) str_buffer += [str(province)] else: self.error(on_error, 'Unable to find a unit, a power or an order to build the order clause') return daide_bytes if order_group_bytes: self.error(on_error, '{} bytes remaining. Order is malformed'.format(len(order_group_bytes))) return daide_bytes self._power_name = str(power) if power else unit.power_name self._str = ' '.join(str_buffer) return remaining_bytes def from_string(self, string, on_error='raise'): """ Builds the clause from a string :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ raise NotImplementedError()