How does SmartHouseManagement have access to all current instantiated objects?
- Is not done via introspection (first approach)
- We couldn’t use the
if __name__ == "__main__"guard this way (objects instantiated inside of guard aren’t in globals)
- We couldn’t use the
- We added a hook in
makethat adds new objects (just dicts) to a globalinstanceslist,SmartHouseManagementclass uses this list to iterate over all objects
What tradeoffs were taken during the project?
findfunction has no guard for circular dependencies- If A inherits from B and B inherits from A, the function will loop forever
- How to solve?
- By adding a flag
visitedto classes
- By adding a flag
In what order does find find implementations of a function?
Class A: _parents = ["Foo", "Bar"]
Class Foo: _parents = ["Foobar", "Baz"]
Class Bar: _parents = ["Foobaz", "Bazbar"]
Lookup Order:
1. Class A
2. Class Foo
3. Class Foobar
4. Class Baz
5. Class Bar
6. Class Foobaz
7. Class Bazbar- Lookup order is DFS, left to right (order of
_parentsarray matters!) - This doesn’t guarantee finding the “most specific” implementation of a function in the whole inheritance tree → First occurrence of function is returned
- Modern languages use more sophisticated approaches
What happens if order of _parents array changes?
⇒ Lookup order changes!
What happens if a function isn’t found in find?
NotImplementedError is raised (same as when calling an abstract function)
What is stored inside an instance? How are multiple inheritance instances constructed?
Inside an instance, only instance-specific-properties are stored PLUS the link to the respective _class dictionary which holds all methods a class implements (are the same for all instances of said class)
Constructing a multiple inheritance instance is done via the | operator (merges dictionaries)
- The constructor method of a class merges the returned dictionaries of all
makecalls of the parent classes of a class with the properties of the constructed class - It overwrites the
_classproperty of the returned dictionaries, that’s why the class-specific dictionary has to come at the end of the|chain ⇒ later dicts in|chain override previous values!
# Thermostat inherits from both Device and Connectable
def thermostat_new(name: str, location: str, base_power: float, status: str, room_temperature: int, target_temperature: int, connected: bool = False,ip: str = "",):
return (
make(Device, name, location, base_power, status)
| make(Connectable, connected, ip)
# Merge of class specific properties at the end!
# Overrides of _class
| {
"_class": Thermostat,
"room_temperature": room_temperature,
"target_temperature": target_temperature,
}
)How does the inheritance look like for Device, Connectable, Light, Thermostat, Camera, SmartHouseManagement?
- Device:
_parents = [] - Connectable:
_parents = [] - Light:
_parents = [Device] - Thermostat:
_parents = [Device, Connectable] - Camera:
_parents = [Device, Connectable] - SmartHouseManagement:
_parents = []
⇒ Device and Connectable are abstract parent classes, not to be actually constructed
- Their constructors are only used inside
Light,ThermostatandCamerathat’s why the constructors ofDeviceandConnectabledon’t raiseNotImplementedErroreven though they are abstract - Constructing
DeviceandConnectableis not added to globalinstanceslist and thus not regarded bySmartHouseManagement
What’s the difference between is and == and what’s the implication for the code?
is(andis not) checks whether two objects are stored at the same memory address- This is why you could have two identical instances of a class and
iswould returnFalsewhen comparing them that way
- This is why you could have two identical instances of a class and
==checks whether two objects or numbers are the same (disregarding whether they point to different locations)
⇒ Implication: When filtering in SmartHouseManagement via search_type for example, the global class dictionary has to be passed in, a copy would not be found, because SmartHouseManagement checks device["_class"] is not search_type :
# Creates a separate object at a different memory address
Light2 = Light.copy()
# False
Light2 is Light
# True
Light2 == LightHow does the call function make sure it is able to call all functions of all instances even if they require different amounts of arguments?
It handles different numbers of arguments via * ⇒ call is called via an arbitrary amount of parameters
- It passes these parameters to the respective method via
*args(or**kwargs) - This means that the arguments passed to
callmust match the method signature of the method we want to call
What is inheritsFrom and is there a tradeoff there?
inheritsFrom is just a simple helper function designed to make the testing in the main guard read a bit easier
- Its tradeoff is, that it only checks single-depth inheritance!!
What are the rules for calculating power consumption?
- 0 when
offfor all devices - Light:
round(device["base_power"] * device["brightness"] / 100) - Thermostat:
return device["base_power"] * abs(device["target_temperature"] - device["room_temperature"] - Camera:
device["base_power"] * device["resolution_factor"]
Requirement for a device to be found in get_all_connected_devices and what does the function return?
Connectableas parentconnected == Truestatus != off- Optional IP match
get_all_connected_devices returns:
A list of: {"description": description, "power": power} dicts
How does test function discovery work?
globals.items()is iterated ⇒ introspection- Prefix is stripped ⇒
test_toggle_statusstored astoggle_statusin dict with all test functions - Only objects which are
callable(= functions) are stored
How does the --select parameter work and how is it implemented?
It runs only those functions whose names contain the value passed into select: python test_smart_house.py --select light runs only functions containing light
Implementation:
- When starting the test suite,
if "--select" in sys.argvis checked (sys.argvis a list) - If so, the index of the
--selectflag is extractedindex = sys.argv.index("--select") - The value passed in for
--selectmust naturally be the next element insys.argv:keyword = sys.argv[index + 1]- If no value is passed,
IndexErroris caught and program ends - If value of
--selectstarts with--also, there is something wrong (flag passed as value), so program returns
- If no value is passed,
- After
keywordhas been successfully extracted, actual running of tests begins. Function calls skip if:keyword is not None and keyword.lower() not in testFunctionName.lower():
How is the test suite timing the function calls?
setUpandtearDowncalled before each function call- Both functions return the current time, the delta is added to the total execution time and printed in the end
What’s a side effect that setUp has?
- Side Effect: anything a function does to anything outside its body ⇒ anything different than calculating a return value and returning it
- The important side effect in
setUpis, that it clears theinstancesarray insmart_house.pyso that every test function starts out with a “fresh”smart_house.py
How are pass, fail and error tracked inside the suite?
- Done via the dictionary
results = {"pass": 0, "fail": 0, "error": 0}- When a function raises an
AssertionError,failis incremented by one - When a function raises an
Exceptionof any other kind,erroris incremented - When the function runs normally,
passis incremented by one
- When a function raises an
How does testing of SmartHouseManagement differ from other tests?
- Tests for
SmartHouseManagementuse mock objects and classes, not constructed viamake! - The classes point to
_fixed_powerand_fixed_descforget_power_consumptionanddescribe_device_fixed_powerand_fixed_descboth returnobj["__power"]andobj["__desc"]
What does the --verbose flag do?
If set, it prints all variables starting with test
- Potential problem here: This block runs every time because it is not protected by main guard ⇒ would also print when just importing the module!