├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── chapter-1 ├── animals │ ├── __init__.py │ ├── cat.py │ ├── cow.py │ ├── dog.py │ ├── horse.py │ └── sheep.py ├── cache │ ├── cache.py │ └── test_cache.py ├── shaghetti │ ├── config.ini │ └── spaghetti.py └── stats │ ├── stats.py │ └── test_stats.py ├── chapter-2 └── inventoryControl │ ├── datastorage.py │ ├── items.json │ ├── main.py │ ├── reportgenerator.py │ └── userinterface.py ├── chapter-3 ├── double.py ├── funkycase.py └── test_module.py ├── chapter-4 ├── charter │ ├── __init__.py │ ├── chart.py │ ├── constants.py │ ├── generator.py │ └── renderers │ │ ├── __init__.py │ │ ├── pdf │ │ ├── __init__.py │ │ ├── bar_series.py │ │ ├── line_series.py │ │ ├── title.py │ │ ├── x_axis.py │ │ └── y_axis.py │ │ ├── png │ │ ├── __init__.py │ │ ├── bar_series.py │ │ ├── line_series.py │ │ ├── title.py │ │ ├── x_axis.py │ │ └── y_axis.py │ │ └── renderer.py └── test_charter.py ├── chapter-5 ├── abstraction │ └── happy_hour.py ├── dynamic imports │ ├── load_module.py │ ├── module_a.py │ ├── module_b.py │ └── module_c.py ├── encapsulation │ └── recipes.py ├── plugins │ ├── load_plugin.py │ └── plugins │ │ ├── plugin_a.py │ │ ├── plugin_b.py │ │ └── plugin_c.py └── wrappers │ ├── detect_unusual_transfers.py │ └── numpy_wrapper.py ├── chapter-6 ├── quantities │ ├── README.txt │ ├── __init__.py │ ├── interface.py │ ├── quantity.py │ └── units.py └── test.py ├── chapter-7 ├── add to path gotcha │ ├── bad_imports.py │ ├── good_imports.py │ └── package │ │ ├── __init__.py │ │ └── module.py ├── module name gotcha │ ├── main.py │ ├── math.py │ └── math.pyc ├── package configuration │ ├── configpackage │ │ ├── __init__.py │ │ └── interface.py │ └── runner.py ├── package data │ ├── datapackage │ │ ├── __init__.py │ │ ├── data │ │ │ └── phone_numbers.txt │ │ └── interface.py │ └── runner.py ├── package globals │ ├── globalspackage │ │ ├── __init__.py │ │ ├── globals.py │ │ └── test.py │ └── runner.py ├── script as module gotcha │ ├── helpers.py │ └── test.py ├── script name gotcha │ └── re.py └── stringutils │ ├── stringutils (buggy).py │ └── stringutils (fixed).py ├── chapter-8 ├── test-package │ ├── MANIFEST │ ├── README.rst │ ├── erikwestra_test_package │ │ ├── __init__.py │ │ └── test.py │ └── setup.py └── test_quantities │ ├── quantities │ ├── README.txt │ ├── __init__.py │ ├── interface.py │ ├── quantity.py │ └── units.py │ └── test_quantities.py └── chapter-9 ├── counter-ranges ├── README.rst ├── counter │ ├── __init__.py │ └── interface.py └── tests.py └── counter ├── README.rst ├── counter ├── __init__.py └── interface.py └── tests.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Packt Publishing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Modular-Programming-with-Python 5 | 6 | 7 | About the Code Samples 8 | ---------------------- 9 | 10 | These code samples all comes from the book [Modular Progarmming with Python](https://www.packtpub.com/application-development/modular-programming-python?utm_source=github&utm_medium=related&utm_campaign=9781785884481). 11 | They are organized by chapter. Please feel free to copy and use these in 12 | whatever way you like -- there are no restrictions at all on the use of these 13 | code samples. 14 | 15 | These code samples will work on any Mac OS X, Windows or Linux computer. While 16 | they are all written to use Python version 3.3 or later, you can modify them to 17 | use Python 2.x with minimal changes. 18 | 19 | To run the code samples from Chapter 4, you will need to install [Reportlab](http://www.reportlab.com/opensource/) and [Pillow](http://python-pillow.org/). To use the "wrappers" example from Chapter 5, you will need to install [NumPy](http://www.numpy.org/). 20 | All the other examples only require the Python Standard Library. 21 | 22 | You can also refer to the following books: 23 | 24 | * [Expert Python Programming](https://www.packtpub.com/application-development/expert-python-programming?utm_source=github&utm_medium=related&utm_campaign=9781847194947) 25 | * [Learning Cython Programming](https://www.packtpub.com/application-development/learning-cython-programming?utm_source=github&utm_medium=related&utm_campaign=9781783280797) 26 | * [Instant Data Intensive Apps with pandas How-to](https://www.packtpub.com/big-data-and-business-intelligence/instant-data-intensive-apps-pandas-how-instant?utm_source=github&utm_medium=related&utm_campaign=9781782165583) 27 | ### Download a free PDF 28 | 29 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
30 |

https://packt.link/free-ebook/9781785884481

-------------------------------------------------------------------------------- /chapter-1/animals/__init__.py: -------------------------------------------------------------------------------- 1 | # Package initialization file. 2 | -------------------------------------------------------------------------------- /chapter-1/animals/cat.py: -------------------------------------------------------------------------------- 1 | # cat.py 2 | 3 | def speak(): 4 | print("meow") 5 | 6 | -------------------------------------------------------------------------------- /chapter-1/animals/cow.py: -------------------------------------------------------------------------------- 1 | # cow.py 2 | 3 | def speak(): 4 | print("moo") 5 | 6 | -------------------------------------------------------------------------------- /chapter-1/animals/dog.py: -------------------------------------------------------------------------------- 1 | # dog.py 2 | 3 | def speak(): 4 | print("woof") 5 | 6 | -------------------------------------------------------------------------------- /chapter-1/animals/horse.py: -------------------------------------------------------------------------------- 1 | # dog.py 2 | 3 | def speak(): 4 | print("neigh") 5 | 6 | -------------------------------------------------------------------------------- /chapter-1/animals/sheep.py: -------------------------------------------------------------------------------- 1 | # dog.py 2 | 3 | def speak(): 4 | print("baa") 5 | 6 | -------------------------------------------------------------------------------- /chapter-1/cache/cache.py: -------------------------------------------------------------------------------- 1 | # cache.py 2 | 3 | import datetime 4 | 5 | MAX_CACHE_SIZE = 100 # Maximum number of entries in the cache. 6 | 7 | ############################################################################# 8 | 9 | def init(): 10 | global _cache 11 | _cache = {} # Maps key to (timestamp, value) tuple. 12 | 13 | 14 | def set(key, value): 15 | global _cache 16 | if key not in _cache and len(_cache) >= MAX_CACHE_SIZE: 17 | _remove_oldest_entry() 18 | _cache[key] = [datetime.datetime.now(), value] 19 | 20 | 21 | def get(key): 22 | global _cache 23 | if key in _cache: 24 | _cache[key][0] = datetime.datetime.now() 25 | return _cache[key][1] 26 | else: 27 | return None 28 | 29 | 30 | def contains(key): 31 | global _cache 32 | return key in _cache 33 | 34 | 35 | def size(): 36 | global _cache 37 | return len(_cache) 38 | 39 | ############################################################################# 40 | 41 | # Private functions: 42 | 43 | def _remove_oldest_entry(): 44 | global _cache 45 | oldest = None 46 | for key in _cache.keys(): 47 | if oldest == None: 48 | oldest = key 49 | elif _cache[key][0] < _cache[oldest][0]: 50 | oldest = key 51 | 52 | if oldest != None: 53 | del _cache[oldest] 54 | 55 | -------------------------------------------------------------------------------- /chapter-1/cache/test_cache.py: -------------------------------------------------------------------------------- 1 | # test_cache.py 2 | 3 | import random 4 | import string 5 | import cache 6 | 7 | def random_string(length): 8 | s = '' 9 | for i in range(length): 10 | s = s + random.choice(string.ascii_letters) 11 | return s 12 | 13 | cache.init() 14 | 15 | for n in range(1000): 16 | while True: 17 | key = random_string(20) 18 | if cache.contains(key): 19 | continue 20 | else: 21 | break 22 | value = random_string(20) 23 | cache.set(key, value) 24 | print("After {} iterations, cache has {} entries".format(n+1, cache.size())) 25 | 26 | -------------------------------------------------------------------------------- /chapter-1/shaghetti/config.ini: -------------------------------------------------------------------------------- 1 | # Configuration file for the spaghetti.py program. 2 | 3 | [config] 4 | 5 | num_data_points=10 6 | 7 | -------------------------------------------------------------------------------- /chapter-1/shaghetti/spaghetti.py: -------------------------------------------------------------------------------- 1 | # spaghetti.py -- an example of bad coding. 2 | 3 | import configparser 4 | 5 | def load_config(): 6 | config = configparser.ConfigParser() 7 | config.read("config.ini") 8 | return config['config'] 9 | 10 | def get_data_from_user(): 11 | config = load_config() 12 | data = [] 13 | for n in range(config.getint('num_data_points')): 14 | value = input("Data point {}: ".format(n+1)) 15 | data.append(value) 16 | return data 17 | 18 | def print_results(results): 19 | for value,num_times in results: 20 | print("{} = {}".format(value, num_times)) 21 | 22 | def analyze_data(): 23 | data = get_data_from_user() 24 | results = {} 25 | for value in data: 26 | try: 27 | results[value] = results[value] + 1 28 | except KeyError: 29 | results[value] = 1 30 | return results 31 | 32 | def sort_results(results): 33 | sorted_results = [] 34 | for value in results.keys(): 35 | sorted_results.append((value, results[value])) 36 | sorted_results.sort() 37 | return sorted_results 38 | 39 | if __name__ == "__main__": 40 | results = analyze_data() 41 | sorted_results = sort_results(results) 42 | print_results(sorted_results) 43 | 44 | -------------------------------------------------------------------------------- /chapter-1/stats/stats.py: -------------------------------------------------------------------------------- 1 | # stats.py 2 | 3 | def init(): 4 | global _stats 5 | _stats = {} 6 | 7 | def event_occurred(event): 8 | global _stats 9 | try: 10 | _stats[event] = _stats[event] + 1 11 | except KeyError: 12 | _stats[event] = 1 13 | 14 | def get_stats(): 15 | global _stats 16 | return sorted(_stats.items()) 17 | -------------------------------------------------------------------------------- /chapter-1/stats/test_stats.py: -------------------------------------------------------------------------------- 1 | # test_stats.py 2 | 3 | import stats 4 | 5 | stats.init() 6 | stats.event_occurred("meal_eaten") 7 | stats.event_occurred("snack_eaten") 8 | stats.event_occurred("meal_eaten") 9 | stats.event_occurred("snack_eaten") 10 | stats.event_occurred("meal_eaten") 11 | stats.event_occurred("diet_started") 12 | stats.event_occurred("meal_eaten") 13 | stats.event_occurred("meal_eaten") 14 | stats.event_occurred("meal_eaten") 15 | stats.event_occurred("diet_abandoned") 16 | stats.event_occurred("snack_eaten") 17 | 18 | for event,num_times in stats.get_stats(): 19 | print("{} occurred {} times".format(event, num_times)) 20 | -------------------------------------------------------------------------------- /chapter-2/inventoryControl/datastorage.py: -------------------------------------------------------------------------------- 1 | # datastorage.py 2 | # 3 | # We store our data in three private globals: 4 | # 5 | # 'items' 6 | # 7 | # The list of current inventory items. Each item in this list is a 8 | # (product_code, location_code) tuple. 9 | # 10 | # 'products' 11 | # 12 | # The list of products we can hold inventory for. Each item in this list 13 | # is a (code, description, desired_number) tuple, where 'code is a code 14 | # identifying the product, 'description' is a string describing that 15 | # product so the user can identify it, and 'desired_number' is the 16 | # desired number of items that the user wants to keep in the inventory. 17 | # 18 | # 'locations' 19 | # 20 | # The list of locations where inventory can be stored. Each item in this 21 | # list is a (code, description) tuple, where 'code is the code for an 22 | # inventory location, and 'description' is a string describing that 23 | # location so that the user knows where it is. 24 | 25 | import json 26 | import os.path 27 | 28 | ############################################################################# 29 | 30 | def init(): 31 | """ Initialize the datastorage module. 32 | """ 33 | _load_items() 34 | 35 | ############################################################################# 36 | 37 | def items(): 38 | """ Return a list of inventory items. 39 | 40 | We return a list of inventory items. Each item in the returned list 41 | will be a (product_code, location_code) tuple. 42 | 43 | Note that the returned list of items are not sorted in any way. 44 | """ 45 | global _items 46 | return _items 47 | 48 | ############################################################################# 49 | 50 | def products(): 51 | """ Return a list of the known products. 52 | 53 | We return a list of (code, description, desired_number) tuples 54 | containing all the known inventory products. 55 | """ 56 | global _products 57 | return _products 58 | 59 | ############################################################################# 60 | 61 | def locations(): 62 | """ Return a list of the known locations where inventory can be stored. 63 | 64 | We return a list of (code, description) tuples containing all the known 65 | inventory locations. 66 | """ 67 | global _locations 68 | return _locations 69 | 70 | ############################################################################# 71 | 72 | def add_item(product_code, location_code): 73 | """ Add an item to the inventory with the given product code and location. 74 | """ 75 | global _items 76 | _items.append((product_code, location_code)) 77 | _save_items() 78 | 79 | ############################################################################# 80 | 81 | def remove_item(product_code, location_code): 82 | """ Remove an inventory item with the given product code from the given location. 83 | 84 | Returns True if and only if the item was successfully removed. 85 | """ 86 | global _items 87 | for i in range(len(_items)): 88 | prod_code,loc_code = _items[i] 89 | if prod_code == product_code and loc_code == location_code: 90 | del _items[i] 91 | _save_items() 92 | return True 93 | return False 94 | 95 | ############################################################################# 96 | 97 | def set_products(products): 98 | """ Set the (currently hardwired) list of inventory products. 99 | 100 | Each item in the 'products' list should be a (code, description, 101 | desired_number) tuple, where 'code is a code identifying the product, 102 | 'description' is a string describing that product so the user can 103 | identify it, and 'desired_number' is the desired number of items that 104 | you want to keep in the inventory. 105 | """ 106 | global _products 107 | _products = products 108 | 109 | ############################################################################# 110 | 111 | def set_locations(locations): 112 | """ Set the (currently hardwired) list of inventory locations. 113 | 114 | Each item in the 'locations' list should be a (code, description) 115 | tuple, where 'code is the code for an inventory location, and 116 | 'description' is a string describing that location so that the user 117 | knows where it is. 118 | """ 119 | global _locations 120 | _locations = locations 121 | 122 | ############################################################################# 123 | # 124 | # Private definitions: 125 | 126 | def _load_items(): 127 | """ Load the list of inventory items from disk. 128 | """ 129 | global _items 130 | 131 | if os.path.exists("items.json"): 132 | f = open("items.json", "r") 133 | _items = json.loads(f.read()) 134 | f.close() 135 | else: 136 | _items = [] 137 | 138 | ############################################################################# 139 | 140 | def _save_items(): 141 | """ Save the list of inventory items to disk. 142 | """ 143 | global _items 144 | 145 | f = open("items.json", "w") 146 | f.write(json.dumps(_items)) 147 | f.close() 148 | 149 | -------------------------------------------------------------------------------- /chapter-2/inventoryControl/items.json: -------------------------------------------------------------------------------- 1 | [["SKU145", "S2A1"], ["SKU123", "S1A1"], ["SKU123", "S1A1"]] -------------------------------------------------------------------------------- /chapter-2/inventoryControl/main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # 3 | # This is the main program for the InventoryControl system. 4 | 5 | import datastorage 6 | import userinterface 7 | import reportgenerator 8 | 9 | ############################################################################# 10 | 11 | def main(): 12 | datastorage.init() 13 | 14 | datastorage.set_products([ 15 | ("SKU123", "4 mm flat-head wood screw", 50), 16 | ("SKU145", "6 mm flat-head wood screw", 50), 17 | ("SKU167", "4 mm countersunk head wood screw", 10), 18 | ("SKU169", "6 mm countersunk head wood screw", 10), 19 | ("SKU172", "4 mm metal self-tapping screw", 20), 20 | ("SKU185", "8 mm metal self-tapping screw", 20), 21 | ]) 22 | 23 | datastorage.set_locations([ 24 | ("S1A1", "Shelf 1, Aisle 1"), 25 | ("S2A1", "Shelf 2, Aisle 1"), 26 | ("S3A1", "Shelf 3, Aisle 1"), 27 | ("S1A2", "Shelf 1, Aisle 2"), 28 | ("S2A2", "Shelf 2, Aisle 2"), 29 | ("S3A2", "Shelf 3, Aisle 2"), 30 | ("BIN1", "Storage Bin 1"), 31 | ("BIN2", "Storage Bin 2"), 32 | ]) 33 | 34 | while True: 35 | action = userinterface.prompt_for_action() 36 | if action == "QUIT": 37 | break 38 | elif action == "ADD": 39 | product = userinterface.prompt_for_product() 40 | if product != None: 41 | location = userinterface.prompt_for_location() 42 | if location != None: 43 | datastorage.add_item(product, location) 44 | elif action == "REMOVE": 45 | product = userinterface.prompt_for_product() 46 | if product != None: 47 | location = userinterface.prompt_for_location() 48 | if location != None: 49 | if not datastorage.remove_item(product, location): 50 | userinterface.show_error("There is no product with " + 51 | "that code at that location!") 52 | elif action == "INVENTORY_REPORT": 53 | report = reportgenerator.generate_inventory_report() 54 | userinterface.show_report(report) 55 | elif action == "REORDER_REPORT": 56 | report = reportgenerator.generate_reorder_report() 57 | userinterface.show_report(report) 58 | 59 | ############################################################################# 60 | 61 | if __name__ == "__main__": 62 | main() 63 | 64 | -------------------------------------------------------------------------------- /chapter-2/inventoryControl/reportgenerator.py: -------------------------------------------------------------------------------- 1 | # reportgenerator.py 2 | # 3 | # This module implements the report-generation logic for the InventoryControl 4 | # system. 5 | 6 | import datastorage 7 | 8 | ############################################################################# 9 | 10 | def generate_inventory_report(): 11 | """ Generate a report of the current inventory levels. 12 | 13 | We return a list of strings containing the report's contents. 14 | """ 15 | # Get the list of known products and locations. 16 | 17 | product_names = {} # Maps product code to product name. 18 | for product_code,name,desired_number in datastorage.products(): 19 | product_names[product_code] = name 20 | 21 | location_names = {} # Maps location code to location name. 22 | for location_code,name in datastorage.locations(): 23 | location_names[location_code] = name 24 | 25 | # Calculate the report's contents. 26 | 27 | grouped_items = {} # Maps product code to dictionary mapping location code 28 | # to number of items with that product at that location. 29 | 30 | for product_code,location_code in datastorage.items(): 31 | if product_code not in grouped_items: 32 | grouped_items[product_code] = {} 33 | 34 | if location_code not in grouped_items[product_code]: 35 | grouped_items[product_code][location_code] = 1 36 | else: 37 | grouped_items[product_code][location_code] += 1 38 | 39 | # Generate the report. 40 | 41 | report = [] 42 | report.append("INVENTORY REPORT") 43 | report.append("") 44 | 45 | for product_code in sorted(grouped_items.keys()): 46 | product_name = product_names[product_code] 47 | report.append("Inventory for product: {} - {}".format(product_code, 48 | product_name)) 49 | report.append("") 50 | 51 | for location_code in sorted(grouped_items[product_code].keys()): 52 | location_name = location_names[location_code] 53 | num_items = grouped_items[product_code][location_code] 54 | report.append(" {} at {} - {}".format(num_items, 55 | location_code, 56 | location_name)) 57 | report.append("") 58 | 59 | return report 60 | 61 | ############################################################################# 62 | 63 | def generate_reorder_report(): 64 | """ Generate a report of the inventory items to re-order. 65 | 66 | We return a list of strings containing the report's contents. 67 | """ 68 | # Get the list of known products and locations. 69 | 70 | product_names = {} # Maps product code to product name. 71 | desired_numbers = {} # maps product code to desired number of items. 72 | 73 | for product_code,name,desired_number in datastorage.products(): 74 | product_names[product_code] = name 75 | desired_numbers[product_code] = desired_number 76 | 77 | # Calculate our current inventory levels. 78 | 79 | num_in_inventory = {} # Maps product code to number of items in inventory. 80 | 81 | for product_code,location_code in datastorage.items(): 82 | if product_code in num_in_inventory: 83 | num_in_inventory[product_code] += 1 84 | else: 85 | num_in_inventory[product_code] = 1 86 | 87 | # Generate the report. 88 | 89 | report = [] 90 | report.append("RE-ORDER REPORT") 91 | report.append("") 92 | 93 | for product_code in sorted(product_names.keys()): 94 | desired_number = desired_numbers[product_code] 95 | current_number = num_in_inventory.get(product_code, 0) 96 | if current_number < desired_number: 97 | product_name = product_names[product_code] 98 | num_to_reorder = desired_number - current_number 99 | report.append(" Re-order {} of {} - {}".format(num_to_reorder, 100 | product_code, 101 | product_name)) 102 | report.append("") 103 | 104 | return report 105 | 106 | -------------------------------------------------------------------------------- /chapter-2/inventoryControl/userinterface.py: -------------------------------------------------------------------------------- 1 | # userinterface.py 2 | # 3 | # This module implements the user interface for the InventoryControl system. 4 | 5 | import datastorage 6 | 7 | ############################################################################# 8 | 9 | def prompt_for_action(): 10 | """ Prompt the user to choose an action to perform. 11 | 12 | We return one of the following action codes: 13 | 14 | "QUIT" 15 | "ADD" 16 | "REMOVE" 17 | "INVENTORY_REPORT" 18 | "REORDER_REPORT" 19 | """ 20 | while True: 21 | print() 22 | print("What would you like to do?") 23 | print() 24 | print(" A = add an item to the inventory.") 25 | print(" R = remove an item from the inventory.") 26 | print(" C = generate a report of the current inventory levels.") 27 | print(" O = generate a report of the inventory items to re-order.") 28 | print(" Q = quit.") 29 | print() 30 | action = input("> ").strip().upper() 31 | if action == "A": return "ADD" 32 | elif action == "R": return "REMOVE" 33 | elif action == "C": return "INVENTORY_REPORT" 34 | elif action == "O": return "REORDER_REPORT" 35 | elif action == "Q": return "QUIT" 36 | else: 37 | print("Unknown action!") 38 | 39 | ############################################################################# 40 | 41 | def prompt_for_product(): 42 | """ Prompt the user to select a product. 43 | 44 | We return the code for the selected product, or None if the user 45 | cancelled. 46 | """ 47 | while True: 48 | print() 49 | print("Select a product:") 50 | print() 51 | n = 1 52 | for code,description,desired_number in datastorage.products(): 53 | print(" {}. {} - {}".format(n, code, description)) 54 | n = n + 1 55 | 56 | s = input("> ").strip() 57 | if s == "": return None 58 | 59 | try: 60 | n = int(s) 61 | except ValueError: 62 | n = -1 63 | 64 | if n < 1 or n > len(datastorage.products()): 65 | print("Invalid option: {}".format(s)) 66 | continue 67 | 68 | product_code = datastorage.products()[n-1][0] 69 | return product_code 70 | 71 | ############################################################################# 72 | 73 | def prompt_for_location(): 74 | """ Prompt the user to select a location. 75 | 76 | We return the code for the selected location, or None if the user 77 | cancelled. 78 | """ 79 | while True: 80 | print() 81 | print("Select a location:") 82 | print() 83 | n = 1 84 | for code,description in datastorage.locations(): 85 | print(" {}. {} - {}".format(n, code, description)) 86 | n = n + 1 87 | 88 | s = input("> ").strip() 89 | if s == "": return None 90 | 91 | try: 92 | n = int(s) 93 | except ValueError: 94 | n = -1 95 | 96 | if n < 1 or n > len(datastorage.locations()): 97 | print("Invalid option: {}".format(s)) 98 | continue 99 | 100 | location_code = datastorage.locations()[n-1][0] 101 | return location_code 102 | 103 | ############################################################################# 104 | 105 | def show_error(err_msg): 106 | """ Display the given error message to the user. 107 | """ 108 | print() 109 | print(err_msg) 110 | print() 111 | 112 | ############################################################################# 113 | 114 | def show_report(report): 115 | """ Display the given report to the user. 116 | 117 | 'report' is a list of strings containing the contents of the report. 118 | """ 119 | print() 120 | for line in report: 121 | print(line) 122 | print() 123 | 124 | -------------------------------------------------------------------------------- /chapter-3/double.py: -------------------------------------------------------------------------------- 1 | def double(n): 2 | return n * 2 3 | 4 | if __name__ == "__main__": 5 | print("double(3) =", double(3)) 6 | 7 | -------------------------------------------------------------------------------- /chapter-3/funkycase.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def funky_case(s): 4 | letters = [] 5 | capitalize = False 6 | for letter in s: 7 | if capitalize: 8 | letters.append(letter.upper()) 9 | else: 10 | letters.append(letter.lower()) 11 | capitalize = not capitalize 12 | return "".join(letters) 13 | 14 | if __name__ == "__main__": 15 | if len(sys.argv) != 2: 16 | print("You must supply exactly one string!") 17 | else: 18 | s = sys.argv[1] 19 | print(funky_case(s)) 20 | 21 | -------------------------------------------------------------------------------- /chapter-3/test_module.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | print("in foo") 3 | 4 | def bar(): 5 | print("in bar") 6 | 7 | my_var = 0 8 | 9 | print("importing test module") 10 | 11 | -------------------------------------------------------------------------------- /chapter-4/charter/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | This is the package initialization file for the Charter package. 4 | 5 | We import the various functions from our sub-modules so they can be 6 | accessed directly from the Charter package. 7 | """ 8 | from .chart import * 9 | from .generator import * 10 | -------------------------------------------------------------------------------- /chapter-4/charter/chart.py: -------------------------------------------------------------------------------- 1 | """ chart.py 2 | 3 | This module implements the chart-related functions for the Charter package. 4 | """ 5 | def new_chart(): 6 | return {} 7 | 8 | def set_title(chart, title): 9 | chart['title'] = title 10 | 11 | def set_x_axis(chart, x_axis): 12 | chart['x_axis'] = x_axis 13 | 14 | def set_y_axis(chart, minimum, maximum, labels): 15 | chart['y_min'] = minimum 16 | chart['y_max'] = maximum 17 | chart['y_labels'] = labels 18 | 19 | def set_series_type(chart, series_type): 20 | chart['series_type'] = series_type 21 | 22 | def set_series(chart, series): 23 | chart['series'] = series 24 | 25 | -------------------------------------------------------------------------------- /chapter-4/charter/constants.py: -------------------------------------------------------------------------------- 1 | """ constants.py 2 | 3 | This module defines various constants used by the Charter package. 4 | """ 5 | CHART_WIDTH = 600 6 | CHART_HEIGHT = 400 7 | TITLE_HEIGHT = 50 8 | X_AXIS_HEIGHT = 50 9 | Y_AXIS_WIDTH = 50 10 | MARGIN = 20 11 | TICKMARK_HEIGHT = 8 12 | -------------------------------------------------------------------------------- /chapter-4/charter/generator.py: -------------------------------------------------------------------------------- 1 | """ generator.py 2 | 3 | This module implements the chart-generation logic for the Charter package. 4 | """ 5 | from PIL import Image, ImageDraw 6 | 7 | from reportlab.pdfgen.canvas import Canvas 8 | 9 | from .constants import * 10 | from .renderers import renderer 11 | 12 | ############################################################################# 13 | 14 | def generate_chart(chart, filename): 15 | if filename.lower().endswith(".pdf"): 16 | format = "pdf" 17 | elif filename.lower().endswith(".png"): 18 | format = "png" 19 | else: 20 | print("Unsupported file format: " + filename) 21 | return 22 | 23 | if format == "pdf": 24 | output = Canvas(filename) 25 | elif format == "png": 26 | image = Image.new("RGB", (CHART_WIDTH, CHART_HEIGHT), 27 | "#ffffff") 28 | output = ImageDraw.Draw(image) 29 | 30 | renderer.draw(format, "title", chart, output) 31 | renderer.draw(format, "x_axis", chart, output) 32 | renderer.draw(format, "y_axis", chart, output) 33 | if chart['series_type'] == "bar": 34 | renderer.draw(format, "bar_series", chart, output) 35 | elif chart['series_type'] == "line": 36 | renderer.draw(format, "line_series", chart, output) 37 | 38 | if format == "pdf": 39 | output.showPage() 40 | output.save() 41 | elif format == "png": 42 | image.save(filename, format="png") 43 | 44 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | Empty package initialization file. 4 | """ 5 | 6 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | Empty package initialization file. 4 | """ 5 | 6 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/bar_series.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.pdf.line_series 2 | # 3 | # Renderer for drawing a bar series onto the chart in PDF format. 4 | 5 | from ...constants import * 6 | 7 | ############################################################################# 8 | 9 | def draw(chart, canvas): 10 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 11 | bucket_width = avail_width / len(chart['x_axis']) 12 | 13 | bottom = X_AXIS_HEIGHT 14 | max_top = CHART_HEIGHT - TITLE_HEIGHT 15 | avail_height = max_top - bottom 16 | 17 | left = Y_AXIS_WIDTH 18 | for y_value in chart['series']: 19 | bar_left = left + MARGIN / 2 20 | bar_width = bucket_width - MARGIN 21 | 22 | y = ((y_value - chart['y_min']) / 23 | (chart['y_max'] - chart['y_min'])) 24 | 25 | bar_height = int(y * avail_height) 26 | 27 | canvas.setStrokeColorRGB(0.25, 0.25, 0.625) 28 | canvas.setFillColorRGB(0.906, 0.906, 0.953) 29 | canvas.rect(bar_left, bottom, bar_width, bar_height, 30 | stroke=True, fill=True) 31 | 32 | left = left + bucket_width 33 | 34 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/line_series.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.pdf.line_series 2 | # 3 | # Renderer for drawing a line series onto the chart in PDF format. 4 | 5 | from ...constants import * 6 | 7 | ############################################################################# 8 | 9 | def draw(chart, canvas): 10 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 11 | bucket_width = avail_width / len(chart['x_axis']) 12 | 13 | bottom = X_AXIS_HEIGHT 14 | max_top = CHART_HEIGHT - TITLE_HEIGHT 15 | avail_height = max_top - bottom 16 | 17 | left = Y_AXIS_WIDTH 18 | prev_y = None 19 | for y_value in chart['series']: 20 | y = ((y_value - chart['y_min']) / 21 | (chart['y_max'] - chart['y_min'])) 22 | 23 | cur_y = bottom + int(y * avail_height) 24 | 25 | if prev_y != None: 26 | canvas.setStrokeColorRGB(0.25, 0.25, 0.625) 27 | canvas.setLineWidth(1) 28 | canvas.line(left - bucket_width / 2, prev_y, 29 | left + bucket_width / 2, cur_y) 30 | 31 | prev_y = cur_y 32 | left = left + bucket_width 33 | 34 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/title.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.pdf.title 2 | # 3 | # Renderer for drawing a title onto the chart in PDF format. 4 | 5 | from ...constants import * 6 | 7 | ############################################################################# 8 | 9 | def draw(chart, canvas): 10 | text_width = canvas.stringWidth(chart['title'], "Helvetica", 24) 11 | text_height = 24 * 1.2 12 | 13 | left = CHART_WIDTH/2 - text_width/2 14 | bottom = CHART_HEIGHT - TITLE_HEIGHT/2 + text_height/2 15 | 16 | canvas.setFont("Helvetica", 24) 17 | canvas.setFillColorRGB(0.25, 0.25, 0.625) 18 | canvas.drawString(left, bottom, chart['title']) 19 | 20 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/x_axis.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.pdf.x_axis 2 | # 3 | # Renderer for drawing an X axis onto the chart in PDF format. 4 | 5 | from ...constants import * 6 | 7 | ############################################################################# 8 | 9 | def draw(chart, canvas): 10 | label_height = 12 * 1.2 11 | 12 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 13 | bucket_width = avail_width / len(chart['x_axis']) 14 | 15 | # Draw main axis line. 16 | 17 | axis_top = X_AXIS_HEIGHT 18 | canvas.setStrokeColorRGB(0.25, 0.25, 0.625) 19 | canvas.setLineWidth(2) 20 | canvas.line(Y_AXIS_WIDTH, axis_top, CHART_WIDTH - MARGIN, axis_top) 21 | 22 | left = Y_AXIS_WIDTH 23 | for bucket_num in range(len(chart['x_axis'])): 24 | 25 | # Draw tickmark. 26 | 27 | canvas.setLineWidth(1) 28 | canvas.line(left, axis_top, left, axis_top - TICKMARK_HEIGHT) 29 | 30 | # Draw label. 31 | 32 | label_width = canvas.stringWidth(chart['x_axis'][bucket_num], 33 | "Helvetica", 12) 34 | label_left = max(left, left + bucket_width/2 - label_width/2) 35 | label_bottom = axis_top - TICKMARK_HEIGHT - 4 - label_height 36 | 37 | canvas.setFont("Helvetica", 12) 38 | canvas.setFillColorRGB(0.0, 0.0, 0.0) 39 | canvas.drawString(label_left, label_bottom, 40 | chart['x_axis'][bucket_num]) 41 | 42 | left = left + bucket_width 43 | 44 | # Draw final tickmark. 45 | 46 | canvas.setStrokeColorRGB(0.25, 0.25, 0.625) 47 | canvas.setLineWidth(1) 48 | canvas.line(left, axis_top, left, axis_top - TICKMARK_HEIGHT) 49 | 50 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/pdf/y_axis.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.pdf.y_axis 2 | # 3 | # Renderer for drawing a Y axis onto the chart in PDF format. 4 | 5 | from ...constants import * 6 | 7 | ############################################################################# 8 | 9 | def draw(chart, canvas): 10 | label_height = 12 * 1.2 11 | 12 | axis_top = CHART_HEIGHT - TITLE_HEIGHT 13 | axis_bottom = X_AXIS_HEIGHT 14 | axis_height = axis_top - axis_bottom 15 | 16 | # Draw main axis line. 17 | 18 | canvas.setStrokeColorRGB(0.25, 0.25, 0.625) 19 | canvas.setLineWidth(2) 20 | canvas.line(Y_AXIS_WIDTH, axis_top, Y_AXIS_WIDTH, axis_bottom) 21 | 22 | for y_value in chart['y_labels']: 23 | y = ((y_value - chart['y_min']) / 24 | (chart['y_max'] - chart['y_min'])) 25 | 26 | y_pos = axis_bottom + int(y * axis_height) 27 | 28 | # Draw tickmark. 29 | 30 | canvas.setLineWidth(1) 31 | canvas.line(Y_AXIS_WIDTH - TICKMARK_HEIGHT, y_pos, 32 | Y_AXIS_WIDTH, y_pos) 33 | 34 | # Draw label. 35 | 36 | label_width = canvas.stringWidth(str(y_value), 37 | "Helvetica", 12) 38 | label_left = Y_AXIS_WIDTH - TICKMARK_HEIGHT - label_width - 4 39 | label_bottom = y_pos - label_height/4 40 | 41 | canvas.setFont("Helvetica", 12) 42 | canvas.setFillColorRGB(0.0, 0.0, 0.0) 43 | canvas.drawString(label_left, label_bottom, str(y_value)) 44 | 45 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | Empty package initialization file. 4 | """ 5 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/bar_series.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.png.bar_series 2 | # 3 | # Renderer for drawing a bar series onto the chart in PNG format. 4 | 5 | from PIL import ImageFont 6 | 7 | from ...constants import * 8 | 9 | ############################################################################# 10 | 11 | def draw(chart, drawer): 12 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 13 | bucket_width = avail_width / len(chart['x_axis']) 14 | 15 | max_top = TITLE_HEIGHT 16 | bottom = CHART_HEIGHT - X_AXIS_HEIGHT 17 | avail_height = bottom - max_top 18 | 19 | left = Y_AXIS_WIDTH 20 | for y_value in chart['series']: 21 | bar_left = left + MARGIN / 2 22 | bar_right = left + bucket_width - MARGIN / 2 23 | 24 | y = ((y_value - chart['y_min']) / 25 | (chart['y_max'] - chart['y_min'])) 26 | 27 | bar_top = max_top + (avail_height - int(y * avail_height)) 28 | 29 | drawer.rectangle([(bar_left, bar_top), 30 | (bar_right + 1, 31 | bottom)], 32 | fill="#e8e8f4", outline="#4040a0") 33 | 34 | left = left + bucket_width 35 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/line_series.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.png.line_series 2 | # 3 | # Renderer for drawing a line series onto the chart in PNG format. 4 | 5 | from PIL import ImageFont 6 | 7 | from ...constants import * 8 | 9 | ############################################################################# 10 | 11 | def draw(chart, drawer): 12 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 13 | bucket_width = avail_width / len(chart['x_axis']) 14 | 15 | max_top = TITLE_HEIGHT 16 | bottom = CHART_HEIGHT - X_AXIS_HEIGHT 17 | avail_height = bottom - max_top 18 | 19 | left = Y_AXIS_WIDTH 20 | prev_y = None 21 | for y_value in chart['series']: 22 | y = ((y_value - chart['y_min']) / 23 | (chart['y_max'] - chart['y_min'])) 24 | 25 | cur_y = max_top + (avail_height - int(y * avail_height)) 26 | 27 | if prev_y != None: 28 | drawer.line([(left - bucket_width / 2, prev_y), 29 | (left + bucket_width / 2), cur_y], 30 | fill="#4040a0", width=1) 31 | prev_y = cur_y 32 | left = left + bucket_width 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/title.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.png.title 2 | # 3 | # Renderer for drawing a title onto the chart in PNG format. 4 | 5 | from PIL import ImageFont 6 | 7 | from ...constants import * 8 | 9 | ############################################################################# 10 | 11 | def draw(chart, drawer): 12 | font = ImageFont.truetype("Helvetica", 24) 13 | text_width,text_height = font.getsize(chart['title']) 14 | 15 | left = CHART_WIDTH/2 - text_width/2 16 | top = TITLE_HEIGHT/2 - text_height/2 17 | 18 | drawer.text((left, top), chart['title'], "#4040a0", font) 19 | 20 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/x_axis.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.png.x_axis 2 | # 3 | # Renderer for drawing an X axis onto the chart in PNG format. 4 | 5 | from PIL import ImageFont 6 | 7 | from ...constants import * 8 | 9 | ############################################################################# 10 | 11 | def draw(chart, drawer): 12 | font = ImageFont.truetype("Helvetica", 12) 13 | label_height = font.getsize("Test")[1] 14 | 15 | avail_width = CHART_WIDTH - Y_AXIS_WIDTH - MARGIN 16 | bucket_width = avail_width / len(chart['x_axis']) 17 | 18 | axis_top = CHART_HEIGHT - X_AXIS_HEIGHT 19 | drawer.line([(Y_AXIS_WIDTH, axis_top), (CHART_WIDTH - MARGIN, axis_top)], 20 | "#4040a0", 2) # Draw main axis line. 21 | 22 | left = Y_AXIS_WIDTH 23 | for bucket_num in range(len(chart['x_axis'])): 24 | drawer.line([(left, axis_top), (left, axis_top + TICKMARK_HEIGHT)], 25 | "#4040a0", 1) # Draw tickmark 26 | 27 | label_width = font.getsize(chart['x_axis'][bucket_num])[0] 28 | label_left = max(left, left + bucket_width/2 - label_width/2) 29 | label_top = axis_top + TICKMARK_HEIGHT + 4 30 | 31 | drawer.text((label_left, label_top), chart['x_axis'][bucket_num], 32 | "#000000", font) 33 | 34 | left = left + bucket_width 35 | 36 | drawer.line([(left, axis_top), (left, axis_top + TICKMARK_HEIGHT)], 37 | "#4040a0", 1) # Draw final tickmark. 38 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/png/y_axis.py: -------------------------------------------------------------------------------- 1 | # charter.renderers.png.y_axis 2 | # 3 | # Renderer for drawing a Y-axis onto the chart in PNG format. 4 | 5 | from PIL import ImageFont 6 | 7 | from ...constants import * 8 | 9 | ############################################################################# 10 | 11 | def draw(chart, drawer): 12 | font = ImageFont.truetype("Helvetica", 12) 13 | label_height = font.getsize("Test")[1] 14 | 15 | axis_top = TITLE_HEIGHT 16 | axis_bottom = CHART_HEIGHT - X_AXIS_HEIGHT 17 | axis_height = axis_bottom - axis_top 18 | 19 | drawer.line([(Y_AXIS_WIDTH, axis_top), 20 | (Y_AXIS_WIDTH, axis_bottom)], 21 | "#4040a0", 2) # Draw main axis line. 22 | 23 | for y_value in chart['y_labels']: 24 | y = ((y_value - chart['y_min']) / 25 | (chart['y_max'] - chart['y_min'])) 26 | 27 | y_pos = axis_top + (axis_height - int(y * axis_height)) 28 | 29 | drawer.line([(Y_AXIS_WIDTH - TICKMARK_HEIGHT, y_pos), 30 | (Y_AXIS_WIDTH, y_pos)], 31 | "#4040a0", 1) # Draw tickmark. 32 | 33 | label_width,label_height = font.getsize(str(y_value)) 34 | label_left = Y_AXIS_WIDTH - TICKMARK_HEIGHT - label_width - 4 35 | label_top = y_pos - label_height / 2 36 | 37 | drawer.text((label_left, label_top), str(y_value), 38 | "#000000", font) 39 | 40 | 41 | -------------------------------------------------------------------------------- /chapter-4/charter/renderers/renderer.py: -------------------------------------------------------------------------------- 1 | """ charter.renderers.renderer 2 | 3 | This module renders a given chart element in a given format. 4 | """ 5 | from .png import title as title_png 6 | from .png import x_axis as x_axis_png 7 | from .png import y_axis as y_axis_png 8 | from .png import bar_series as bar_series_png 9 | from .png import line_series as line_series_png 10 | 11 | from .pdf import title as title_pdf 12 | from .pdf import x_axis as x_axis_pdf 13 | from .pdf import y_axis as y_axis_pdf 14 | from .pdf import bar_series as bar_series_pdf 15 | from .pdf import line_series as line_series_pdf 16 | 17 | renderers = { 18 | 'png' : { 19 | 'title' : title_png, 20 | 'x_axis' : x_axis_png, 21 | 'y_axis' : y_axis_png, 22 | 'bar_series' : bar_series_png, 23 | 'line_series' : line_series_png 24 | }, 25 | 'pdf' : { 26 | 'title' : title_pdf, 27 | 'x_axis' : x_axis_pdf, 28 | 'y_axis' : y_axis_pdf, 29 | 'bar_series' : bar_series_pdf, 30 | 'line_series' : line_series_pdf 31 | } 32 | } 33 | 34 | def draw(format, element, chart, output): 35 | renderers[format][element].draw(chart, output) 36 | 37 | -------------------------------------------------------------------------------- /chapter-4/test_charter.py: -------------------------------------------------------------------------------- 1 | import charter 2 | chart = charter.new_chart() 3 | charter.set_title(chart, "Wild Parrot Deaths per Year") 4 | charter.set_x_axis(chart, 5 | ["2009", "2010", "2011", "2012", "2013", 6 | "2014", "2015"]) 7 | charter.set_y_axis(chart, minimum=0, maximum=700, 8 | labels=[0, 100, 200, 300, 400, 500, 600, 700]) 9 | charter.set_series(chart, [250, 270, 510, 420, 680, 580, 450]) 10 | charter.set_series_type(chart, "bar") 11 | charter.generate_chart(chart, "chart.pdf") 12 | 13 | -------------------------------------------------------------------------------- /chapter-5/abstraction/happy_hour.py: -------------------------------------------------------------------------------- 1 | # happy_hour.py 2 | # 3 | # This program demonstrates how to calculate "happy hour", where happy hour 4 | # takes place each day between 5 and 6 pm, except on Sundays, Christmas Day and 5 | # Easter. 6 | 7 | import datetime 8 | 9 | ############################################################################# 10 | 11 | def is_happy_hour(): 12 | today = datetime.date.today() 13 | 14 | easter_sunday = calc_easter_sunday(today.year) 15 | easter_friday = easter_sunday - datetime.timedelta(days=2) 16 | 17 | if today == easter_friday: 18 | return False 19 | 20 | if today.month == 12 and today.day == 25: # Christmas day. 21 | return False 22 | 23 | if today.weekday() == 6: # Sunday. 24 | return False 25 | 26 | if datetime.datetime.now().hour == 17: # 5pm. 27 | return True 28 | 29 | return False 30 | 31 | ############################################################################# 32 | 33 | # The following function calculates Easter Sunday for a given year, returning a 34 | # datetime.date object. This code is taken from: 35 | # 36 | # http://code.activestate.com/recipes/576517 37 | 38 | def calc_easter_sunday(year): 39 | a = year % 19 40 | b = year // 100 41 | c = year % 100 42 | d = (19 * a + b - b // 4 - ((b - (b + 8) // 25 + 1) // 3) + 15) % 30 43 | e = (32 + 2 * (b % 4) + 2 * (c // 4) - d - (c % 4)) % 7 44 | f = d + e - 7 * ((a + 11 * d + 22 * e) // 451) + 114 45 | month = f // 31 46 | day = f % 31 + 1 47 | return datetime.date(year, month, day) 48 | 49 | ############################################################################# 50 | 51 | if __name__ == "__main__": 52 | # Testing code. 53 | 54 | if is_happy_hour(): 55 | print "It's happy hour!" 56 | else: 57 | print "It's not happy hour." 58 | 59 | -------------------------------------------------------------------------------- /chapter-5/dynamic imports/load_module.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | module_name = input("Load module: ") 4 | if module_name != "": 5 | module = importlib.import_module(module_name) 6 | module.say_hello() 7 | 8 | -------------------------------------------------------------------------------- /chapter-5/dynamic imports/module_a.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from module_a") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/dynamic imports/module_b.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from module_b") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/dynamic imports/module_c.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from module_c") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/encapsulation/recipes.py: -------------------------------------------------------------------------------- 1 | # recipes.py 2 | # 3 | # This module demonstrate the concept of encapsulation within a Python module. 4 | 5 | def new(name, num_servings): 6 | """ Create and return a new recipe. 7 | """ 8 | return {'name' : name, 9 | 'num_servings' : num_servings, 10 | 'instructions' : [], 11 | 'ingredients' : []} 12 | 13 | 14 | def add_instruction(recipe, instruction): 15 | """ Add an instruction to the recipe. 16 | """ 17 | recipe['instructions'].append(instruction) 18 | 19 | 20 | def add_ingredient(recipe, ingredient, amount, units): 21 | """ Add an ingredient to the recipe. 22 | """ 23 | recipe['ingredients'].append({'ingredient' : ingredient, 24 | 'amount' : amount, 25 | 'units' : units}) 26 | 27 | 28 | def get_name(recipe): 29 | return recipe['name'] 30 | 31 | 32 | def get_num_servings(recipe): 33 | return recipe['num_servings'] 34 | 35 | 36 | def get_instructions(recipe): 37 | return recipe['instructions'] 38 | 39 | 40 | def get_ingredients(recipe): 41 | return recipe['ingredients'] 42 | 43 | 44 | def to_string(recipe, num_servings): 45 | """ Return a list of strings with a description of this recipe. 46 | 47 | The recipe will be customized for the given number of servings. 48 | """ 49 | s = [] 50 | s.append("Recipe for {}, {} servings:".format(recipe['name'], 51 | num_servings)) 52 | s.append("") 53 | s.append("Ingredients:") 54 | s.append("") 55 | for ingredient in recipe['ingredients']: 56 | s.append(" {} - {} {}".format(ingredient['ingredient'], 57 | ingredient['amount'] * num_servings / 58 | recipe['num_servings'], 59 | ingredient['units'])) 60 | s.append("") 61 | s.append("Instructions:") 62 | s.append("") 63 | for i,instruction in enumerate(recipe['instructions']): 64 | s.append("{}. {}".format(i+1, instruction)) 65 | 66 | return s 67 | 68 | -------------------------------------------------------------------------------- /chapter-5/plugins/load_plugin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | import os.path 4 | 5 | plugin_name = input("Load plugin: ") 6 | if plugin_name != "": 7 | plugin_dir = os.path.join(os.path.dirname(__file__), "plugins") 8 | sys.path.append(plugin_dir) 9 | plugin = importlib.import_module(plugin_name) 10 | plugin.say_hello() 11 | 12 | -------------------------------------------------------------------------------- /chapter-5/plugins/plugins/plugin_a.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from plugin_a") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/plugins/plugins/plugin_b.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from plugin_b") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/plugins/plugins/plugin_c.py: -------------------------------------------------------------------------------- 1 | def say_hello(): 2 | print("Hello from plugin_c") 3 | 4 | -------------------------------------------------------------------------------- /chapter-5/wrappers/detect_unusual_transfers.py: -------------------------------------------------------------------------------- 1 | # detect_unusual_transfers.py 2 | 3 | import random 4 | import numpy_wrapper as npw 5 | 6 | ############################################################################# 7 | 8 | BANK_CODES = ["AMERUS33", "CERYUS33", "EQTYUS44", 9 | "LOYDUS33", "SYNEUS44", "WFBIUS6S"] 10 | 11 | BRANCH_IDS = ["125000249", "125000252", "125000371", 12 | "125000402", "125000596", "125001067"] 13 | 14 | ############################################################################# 15 | 16 | def main(): 17 | """ Our main program. 18 | """ 19 | # Create 10,000 random transfers. 20 | 21 | days = [1, 2, 3, 4, 5, 6, 7, 8] 22 | transfers = [] # List of (day, bank_code, branch_id, amount) tuples. 23 | 24 | for i in range(10000): 25 | day = random.choice(days) 26 | bank_code = random.choice(BANK_CODES) 27 | branch_id = random.choice(BRANCH_IDS) 28 | amount = random.randint(1000, 1000000) 29 | 30 | transfers.append((day, bank_code, branch_id, amount)) 31 | 32 | # Now process the transfers, grouping them by day and building a NumPy 33 | # array mapping each branch ID and bank code combination to the total for 34 | # that branch and bank for that day. 35 | 36 | transfers_by_day = {} 37 | for day in days: 38 | transfers_by_day[day] = npw.new(num_rows=len(BANK_CODES), 39 | num_cols=len(BRANCH_IDS)) 40 | 41 | for day,bank_code,branch_id,amount in transfers: 42 | array = transfers_by_day[day] 43 | row = BRANCH_IDS.index(branch_id) 44 | col = BANK_CODES.index(bank_code) 45 | array[row][col] = array[row][col] + amount 46 | 47 | # Get the most recent day. 48 | 49 | latest_day = max(days) 50 | 51 | # Collect the transfers for all days other than the latest one. 52 | 53 | transfers_to_average = [] 54 | for day in days: 55 | if day != latest_day: 56 | transfers_to_average.append(transfers_by_day[day]) 57 | 58 | # Get the transfers for the current day. 59 | 60 | current = transfers_by_day[latest_day] 61 | 62 | # Calculate the average for each day other than the last one. 63 | 64 | average = npw.average(transfers_to_average) 65 | 66 | # Find the entries in the current day which are more than 150% of the 67 | # average. 68 | 69 | unusual_transfers = current > average * 1.5 70 | 71 | for row,col in npw.get_indices(unusual_transfers): 72 | branch_id = BRANCH_IDS[row] 73 | bank_code = BANK_CODES[col] 74 | average_amt = int(average[row][col]) 75 | current_amt = current[row][col] 76 | 77 | print("Branch {} transferred ${:,d}".format(branch_id, 78 | current_amt) + 79 | " to bank {}, average = ${:,d}".format(bank_code, 80 | average_amt)) 81 | 82 | ############################################################################# 83 | 84 | if __name__ == "__main__": 85 | main() 86 | 87 | -------------------------------------------------------------------------------- /chapter-5/wrappers/numpy_wrapper.py: -------------------------------------------------------------------------------- 1 | # numpy_wrapper 2 | # 3 | # A wrapper around the NumPy library that makes it easier to use for simple 4 | # array mathematics. 5 | 6 | import numpy 7 | 8 | def new(num_rows, num_cols): 9 | return numpy.zeros((num_rows, num_cols), dtype=numpy.int32) 10 | 11 | def average(arrays_to_average): 12 | return numpy.mean(numpy.array(arrays_to_average), axis=0) 13 | 14 | def get_indices(array): 15 | return numpy.transpose(array.nonzero()) 16 | 17 | -------------------------------------------------------------------------------- /chapter-6/quantities/README.txt: -------------------------------------------------------------------------------- 1 | About the `quantities` package 2 | ------------------------------ 3 | 4 | The `quantities` package allows you to define and work with various quantities. 5 | A quantity is a number along with an associated unit, for example: 6 | 7 | 6 inches 8 | 21.2 square kilometers 9 | 0.75 cups 10 | 11 | Using this package, you can define new quantities using the `quantities.new()` 12 | function, or convert a string into a quantity using `quantities.parse()`. For 13 | example: 14 | 15 | >>> q1 = quantities.new(6, "inch") 16 | >>> q2 = quantities.parse("21.2 square kilometers") 17 | >>> q3 = quantities.new(0.75, "cups") 18 | 19 | You can then retrieve information about these quantities by calling the 20 | appropriate functions, for example: 21 | 22 | >>> print(quantities.kind(q1)) 23 | length 24 | >>> print(quantities.value(q2)) 25 | 25 26 | >>> print(quantities.units(q3) 27 | cup 28 | 29 | You can also print a quantity directly to get a summary of its contents: 30 | 31 | >>> print(q1) 32 | 6 inch 33 | 34 | Once you have a quantity, you can convert it into a new quantity in any 35 | compatible unit. For example: 36 | 37 | >>> print(quantities.convert(q1, "mm")) 38 | 152.4 39 | >>> print(quantities.convert(q2, "hectare")) 40 | 100.0 41 | >>> print(quantities.convert(q3, "inch")) 42 | Traceback (most recent call last): 43 | File "", line 1, in 44 | TypeError: float() argument must be a string or a number 45 | ValueError: It's impossible to convert cups into inches! 46 | 47 | Finally, you can retrieve a list of the available kinds of units by calling 48 | `quantities.supported_kinds()`, and get a list of the available units of a 49 | given kind by calling `quantities.supported_units()`. For example: 50 | 51 | >>> print(quantities.supported_kinds()) 52 | ("weight", "length", "area", "volume") 53 | >>> print(quantities.supported_units("weight")) 54 | ("gram", "kilogram", "ounce", "pound") 55 | 56 | 57 | Installing and using the `quantities` package 58 | --------------------------------------------- 59 | 60 | The `quantities` package has no external dependencies, and is compatible with 61 | both Python 2 and Python 3. To install it, simply place the `quantities` 62 | package directory somewhere on your Python path. 63 | 64 | Before you can create and use quantity values, make sure you initialize the 65 | package by calling the `quantities.init()` function. This function takes a 66 | `locale` value as its parameter; the locale is used to customize the spelling 67 | of the various units, as well as allow for different unit conversion factors 68 | depending on where in the world you are. For example, in the US a cup is 69 | measured as 236.5882365 cubic centimeters, while in the rest of the world a cup 70 | is 250 cubic centimeters. 71 | 72 | The `quantities` package currently supports two locales: "us" and 73 | "international". Make sure you choose the appropriate locale when calling the 74 | `quantities.init()` function to initialize the package. 75 | 76 | -------------------------------------------------------------------------------- /chapter-6/quantities/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | This is the package initialization file for the `quantities` package. 4 | """ 5 | from .interface import * 6 | 7 | -------------------------------------------------------------------------------- /chapter-6/quantities/interface.py: -------------------------------------------------------------------------------- 1 | """ quantities.interface 2 | 3 | This module defines the public interface for the `quantities` package. 4 | """ 5 | from .units import UNITS, localize, find_unit 6 | from .quantity import Quantity 7 | 8 | ############################################################################# 9 | 10 | def init(locale): 11 | """ Initialize the 'quantities' package. 12 | 13 | 'locale' is the locale to use for calculating and parsing quantities. 14 | The following locale values are currently supported: 15 | 16 | "us" 17 | "international" 18 | """ 19 | global _locale 20 | _locale = locale 21 | 22 | 23 | def new(value, units): 24 | """ Create and return a new Quantity object. 25 | """ 26 | global _locale 27 | kind,unit = find_unit(units, _locale) 28 | if kind == None: 29 | raise ValueError("Unknown unit: {}".format(units)) 30 | 31 | return Quantity(value, localize(unit['name'], _locale)) 32 | 33 | 34 | def parse(s): 35 | """ Attempt to parse the given string into a Quantity. 36 | 37 | Raises a ValueError if the string can't be parsed into a quantity using 38 | the current locale. 39 | """ 40 | global _locale 41 | 42 | sValue,sUnits = s.split(" ", maxsplit=1) 43 | value = float(sValue) 44 | 45 | kind,unit = find_unit(sUnits, _locale) 46 | if kind == None: 47 | raise ValueError("Unknown unit: {}".format(sUnits)) 48 | 49 | return Quantity(value, localize(unit['name'], _locale)) 50 | 51 | 52 | def kind(q): 53 | """ Return the kind of unit represented by the given quantity. 54 | """ 55 | global _locale 56 | kind,unit = find_unit(q.units, _locale) 57 | return kind 58 | 59 | 60 | def value(q): 61 | """ Return the value of the given quantity. 62 | """ 63 | return q.value 64 | 65 | 66 | def units(q): 67 | """ Return the units for the given quantity. 68 | """ 69 | return q.units 70 | 71 | 72 | def convert(q, units): 73 | """ Attempt to convert the given quantity into the given units. 74 | 75 | Upon completion, we return a new quantity converted into the given 76 | units. If the quantity cannot be converted (for example, because the 77 | units are incompatible), we raise a ValueError. 78 | """ 79 | global _locale 80 | 81 | src_kind,src_units = find_unit(q.units, _locale) 82 | dst_kind,dst_units = find_unit(units, _locale) 83 | 84 | if src_kind == None: 85 | raise ValueError("Unknown units: {}".format(q.units)) 86 | if dst_kind == None: 87 | raise ValueError("Unknown units: {}".format(units)) 88 | 89 | if src_kind != dst_kind: 90 | raise ValueError("It's impossible to convert {} into {}!".format( 91 | localize(src_units['plural'], _locale), 92 | localize(dst_units['plural'], _locale))) 93 | 94 | num_units = q.value * src_units['num_units'] / dst_units['num_units'] 95 | return Quantity(num_units, localize(dst_units['name'], _locale)) 96 | 97 | 98 | def supported_kinds(): 99 | """ Return a list of the various kinds of units that we support. 100 | """ 101 | return list(UNITS.keys()) 102 | 103 | 104 | def supported_units(kind): 105 | """ Return a list of the various units of the given kind that we support. 106 | """ 107 | global _locale 108 | 109 | units = [] 110 | for unit in UNITS.get(kind, []): 111 | units.append(localize(unit['name'], _locale)) 112 | return units 113 | 114 | -------------------------------------------------------------------------------- /chapter-6/quantities/quantity.py: -------------------------------------------------------------------------------- 1 | """ quantities.quantitity 2 | 3 | This module encapsulates the concept of a `quantity`. A quantity is a 4 | number along with the units that number is in. 5 | """ 6 | class Quantity(object): 7 | def __init__(self, value, units): 8 | self.value = value 9 | self.units = units 10 | 11 | def __str__(self): 12 | return "{} {}".format(self.value, self.units) 13 | 14 | -------------------------------------------------------------------------------- /chapter-6/quantities/units.py: -------------------------------------------------------------------------------- 1 | """ units.py 2 | 3 | This module defins the various units supported by the `quantities` package. 4 | 5 | The 'UNITS' global holds all the defined units. This is a dictionary 6 | mapping the kind of units (ie, "weight", "length", etc) to a list of the 7 | known units of that kind. 8 | 9 | For each unit, the list item will be a dictionary with the following 10 | entries: 11 | 12 | 'abbreviation' 13 | 14 | The abbreviation for this unit. 15 | 16 | 'name' 17 | 18 | The singular name for this unit. 19 | 20 | 'plural' 21 | 22 | The plural form of the name. 23 | 24 | 'num_units' 25 | 26 | The conversion factor to apply to this unit. Note that one unit of 27 | each kind must have a conversation factor of 1, and all other units 28 | of that kind must be a multiple of that unit. 29 | 30 | Note that any of these four values can either be a single value, or a 31 | dictionary used to localize that value. If a value (ie, an abbreviation, a 32 | name, a plural name, or the number of units) is localized, then the entry 33 | will be a dictionary mapping each locale to the value to use for that 34 | locale. 35 | 36 | Locales are used to customize the names and abbreviations used for a unit 37 | depending on where in the world the user is. They are also use to 38 | distingush between US measurements and international measurements, in the 39 | cases where they differ. The following locales are currently supported: 40 | 41 | us 42 | international 43 | """ 44 | 45 | UNITS = {} 46 | 47 | ############################################################################# 48 | 49 | # Helper functions: 50 | 51 | def by_locale(value_for_us, value_for_international): 52 | """ Return a dictionary mapping "us" and "international" to the two values. 53 | 54 | This is used to create locale-specific values within our UNITS. 55 | """ 56 | return {"us" : value_for_us, 57 | "international" : value_for_international} 58 | 59 | 60 | def unit(*args): 61 | """ Create a unit dictionary. 62 | 63 | Call with (abbreviation, name, units) or (abbreviation, name, plural 64 | name, units). If the plural name is not given, it is calculated by 65 | adding "s" to the singular name. Note that the abbreviation, name, 66 | plural name (if given) and units can all be either a value or a 67 | dictionary mapping locales to values. 68 | """ 69 | if len(args) == 3: 70 | abbreviation = args[0] 71 | name = args[1] 72 | 73 | if isinstance(name, dict): 74 | plural = {} 75 | for key,value in name.items(): 76 | plural[key] = value + "s" 77 | else: 78 | plural = name + "s" 79 | 80 | num_units = args[2] 81 | elif len(args) == 4: 82 | abbreviation = args[0] 83 | name = args[1] 84 | plural = args[2] 85 | num_units = args[3] 86 | else: 87 | raise RuntimeError("Bad arguments to unit(): {}".format(args)) 88 | 89 | return {'abbreviation' : abbreviation, 90 | 'name' : name, 91 | 'plural' : plural, 92 | 'num_units' : num_units} 93 | 94 | 95 | def units(kind, *units_to_add): 96 | """ Add a list of units to the UNITS global. 97 | 98 | All the added units will be of given kind. 99 | """ 100 | if kind not in UNITS: 101 | UNITS[kind] = [] 102 | 103 | for unit in units_to_add: 104 | UNITS[kind].append(unit) 105 | 106 | ############################################################################# 107 | 108 | # Units of weight: 109 | 110 | units("weight", 111 | unit("g", "gram", 1), 112 | unit("kg", "kilogram", 1000), 113 | unit("oz", "ounce", 28.349523125), 114 | unit("lb", "pound", 453.59237)) 115 | 116 | # Units of length: 117 | 118 | units("length", 119 | unit("mm", by_locale("millimeter", "millimetre"), 0.1), 120 | unit("cm", by_locale("centimeter", "centimetre"), 1), 121 | unit("m", by_locale("meter", "metre"), 100), 122 | unit("km", by_locale("kilometer", "kilometre"), 100000), 123 | unit("in", "inch", "inches", 2.54), 124 | unit("ft", "foot", "feet", 30.48), 125 | unit("yd", "yard", 91.44), 126 | unit("mi", "mile", 160934.4)) 127 | 128 | # Units of area: 129 | 130 | units("area", 131 | unit("sq cm", by_locale("square centimeter", 132 | "square centimetre"), 0.0001), 133 | unit("sq m", by_locale("square meter", 134 | "square metre"), 1), 135 | unit("ha", "hectare", 10000), 136 | unit("sq km", by_locale("square kilometer", 137 | "square kilometre"), 1000000), 138 | unit("sq in", "square inch", "square inches", 0.00064516), 139 | unit("sq ft", "square foot", "square feet", 0.09290304), 140 | unit("sq mi", "square mile", 2589988.110336), 141 | unit("a", "acre", 4046.8564224)) 142 | 143 | # Units of volume: 144 | 145 | units("volume", 146 | unit("cc", by_locale("cubic centimeter", 147 | "cubic centimetre"), 1), 148 | unit("l", by_locale("liter", 149 | "litre"), 1000), 150 | unit("ml", by_locale("milliliter", 151 | "millilitre"), 1), 152 | unit("c", "cup", by_locale(236.5882365, 250)), 153 | unit("dsp", "dessert spoon", by_locale(9.857843187066, 11.8387809397)), 154 | unit("tsp", "teaspoon", by_locale(4.92892159375, 5.0)), 155 | unit("tbsp", "tablespoon", by_locale(14.78676478125, 15.0)), 156 | unit("cu ft", "cubic foot", "cubic feet", 28316.846592), 157 | unit("cu in", "cubic inch", "cubic inches", 16.387064), 158 | unit("cu yd", "cubic yard", 764554.857984), 159 | unit("fl oz", "fluid ounce", by_locale(29.5735295625, 28.4130625)), 160 | unit("gal", "gallon", by_locale(3785.411784, 4546.09)), 161 | unit("pt", "pint", by_locale(473.176473, 568.26125)), 162 | unit("qt", "quart", by_locale(946.352946, 1136.5225))) 163 | 164 | ############################################################################# 165 | 166 | def localize(value, locale): 167 | """ Return the value appropriate for the current locale. 168 | 169 | This is used to retrieve the appropriate localized version of a value 170 | defined within a unit's dictionary. 171 | 172 | If 'value' is a dictionary, we assume it is a mapping of locale names 173 | to values, so we select the appropriate dictionary entry based on the 174 | locale. Otherwise, we return 'value' directly, as the value hasn't 175 | been localized. 176 | """ 177 | if isinstance(value, dict): 178 | return value.get(locale) 179 | else: 180 | return value 181 | 182 | 183 | def find_unit(s, locale): 184 | """ Find the unit with the given name or abbrevation. 185 | 186 | We search through the UNITS dictionary for any unit which has a 187 | (possibly localized) singular or plural name or abbreviation matching 188 | the string 's'. If we find one, we return a (kind, unit) tuple, where 189 | 'kind' is the kind of unit and 'unit' is the dictionary entry for 190 | this unit. 191 | 192 | If we can't find any unit with that name or abbreviation, we return 193 | (None, None). 194 | """ 195 | s = s.lower() 196 | for kind in UNITS.keys(): 197 | for unit in UNITS[kind]: 198 | if (s == localize(unit['abbreviation'], locale).lower() or 199 | s == localize(unit['name'], locale).lower() or 200 | s == localize(unit['plural'], locale).lower()): 201 | # Success! 202 | return (kind, unit) 203 | 204 | return (None, None) # Not found. 205 | 206 | -------------------------------------------------------------------------------- /chapter-6/test.py: -------------------------------------------------------------------------------- 1 | # test.py 2 | # 3 | # Simple testing program for the quantities package. 4 | 5 | import quantities 6 | 7 | quantities.init("us") 8 | 9 | #q = quantities.new(3, "inches") 10 | #print(q) 11 | #print(quantities.kind(q)) 12 | #print(quantities.convert(q, "cm")) 13 | # 14 | #q1 = quantities.new(6, "inch") 15 | #q2 = quantities.new(10, "cm") 16 | #print(quantities.add(q1, q2)) 17 | #print(quantities.subtract(q1, q2)) 18 | 19 | print(quantities.parse("15 liters")) 20 | 21 | -------------------------------------------------------------------------------- /chapter-7/add to path gotcha/bad_imports.py: -------------------------------------------------------------------------------- 1 | # This program demonstrates how adding a package directly to sys.path can cause 2 | # problems with double-importing modules. 3 | 4 | import os.path 5 | import sys 6 | 7 | cur_dir = os.path.abspath(os.path.dirname(__file__)) 8 | package_dir = os.path.join(cur_dir, "package") 9 | 10 | sys.path.insert(1, package_dir) # Add package directly to path. DON'T DO THIS! 11 | 12 | print("Calling import package.module as module...") 13 | import package.module as module 14 | print("Calling import module...") 15 | import module 16 | 17 | -------------------------------------------------------------------------------- /chapter-7/add to path gotcha/good_imports.py: -------------------------------------------------------------------------------- 1 | # This program demonstrates how importing a module more than once won't lead to 2 | # multiple copies of that module. 3 | 4 | print("Calling import package.module...") 5 | import package.module 6 | print("Calling import package.module as module...") 7 | import package.module as module 8 | print("Calling from package import module...") 9 | from package import module 10 | -------------------------------------------------------------------------------- /chapter-7/add to path gotcha/package/__init__.py: -------------------------------------------------------------------------------- 1 | # This is an empty package initialization file. 2 | -------------------------------------------------------------------------------- /chapter-7/add to path gotcha/package/module.py: -------------------------------------------------------------------------------- 1 | # This is a test module to demonstrate why you should never add a package (or a 2 | # sub-directory of a package) directly to sys.path. 3 | # 4 | # When imported, this module prints out a message. This shows that the module 5 | # has been initialized. 6 | 7 | print("### Initializing module.py ###") 8 | -------------------------------------------------------------------------------- /chapter-7/module name gotcha/main.py: -------------------------------------------------------------------------------- 1 | # This program demonstrates how a poorly-named module within a program (in this 2 | # case, "math.py" can mask a module in the Python Standard Library, causing 3 | # subtle problems. 4 | 5 | import random 6 | print(random.choice(["yes", "no"])) 7 | 8 | -------------------------------------------------------------------------------- /chapter-7/module name gotcha/math.py: -------------------------------------------------------------------------------- 1 | # This is a badly-named module. It might do math-related things, but by having 2 | # the same name as a Standard Library module, it masks that module, causing 3 | # other parts of the Python Standard Library to crash. 4 | 5 | def double(n): 6 | return n * 2 7 | 8 | -------------------------------------------------------------------------------- /chapter-7/module name gotcha/math.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Modular-Programming-with-Python/ba43042b7ef6278d4119785f1dbccd19994fa812/chapter-7/module name gotcha/math.pyc -------------------------------------------------------------------------------- /chapter-7/package configuration/configpackage/__init__.py: -------------------------------------------------------------------------------- 1 | # Package initialization file. 2 | 3 | from .interface import * 4 | 5 | -------------------------------------------------------------------------------- /chapter-7/package configuration/configpackage/interface.py: -------------------------------------------------------------------------------- 1 | # This module defines the public interface for our "configpackage" package. 2 | 3 | def init(config): 4 | global settings 5 | 6 | settings = {} 7 | settings['log_errors'] = config.get("log_errors", False) 8 | settings['db_password'] = config.get("db_password", "") 9 | 10 | 11 | def test(): 12 | global settings 13 | 14 | if settings['log_errors']: 15 | print("Error logging enabled") 16 | else: 17 | print("Error logging disabled") 18 | 19 | -------------------------------------------------------------------------------- /chapter-7/package configuration/runner.py: -------------------------------------------------------------------------------- 1 | # Simple script to show how an application can configure a package. 2 | 3 | import configpackage 4 | configpackage.init({'log_errors': True}) 5 | configpackage.test() 6 | 7 | -------------------------------------------------------------------------------- /chapter-7/package data/datapackage/__init__.py: -------------------------------------------------------------------------------- 1 | # Package initialization file. 2 | 3 | from .interface import * 4 | -------------------------------------------------------------------------------- /chapter-7/package data/datapackage/data/phone_numbers.txt: -------------------------------------------------------------------------------- 1 | (123) 456-7890 2 | (123) 921-0487 3 | (167) 201-4494 4 | (192) 549-2024 5 | (502) 772-9903 6 | -------------------------------------------------------------------------------- /chapter-7/package data/datapackage/interface.py: -------------------------------------------------------------------------------- 1 | # This module implements the public interface for the "datapackage" directory. 2 | # 3 | # This demonstrates how a Python package can contain data files, which can then 4 | # be accessed as desired. 5 | 6 | import os.path 7 | 8 | def get_phone_numbers(): 9 | phone_numbers = [] 10 | cur_dir = os.path.abspath(os.path.dirname(__file__)) 11 | file = open(os.path.join(cur_dir, "data", "phone_numbers.txt")) 12 | for line in file: 13 | phone_numbers.append(line.strip()) 14 | file.close() 15 | return phone_numbers 16 | 17 | -------------------------------------------------------------------------------- /chapter-7/package data/runner.py: -------------------------------------------------------------------------------- 1 | # Simple script to load and run datapackage.get_phone_numbers() function. 2 | 3 | import datapackage 4 | print(datapackage.get_phone_numbers()) 5 | 6 | -------------------------------------------------------------------------------- /chapter-7/package globals/globalspackage/__init__.py: -------------------------------------------------------------------------------- 1 | # Package initialization file. 2 | -------------------------------------------------------------------------------- /chapter-7/package globals/globalspackage/globals.py: -------------------------------------------------------------------------------- 1 | # This module defines various global variables which can be shared any module 2 | # within the current package. 3 | 4 | # Define our package globals, giving them a suitable default value. 5 | 6 | language = None 7 | currency = None 8 | 9 | -------------------------------------------------------------------------------- /chapter-7/package globals/globalspackage/test.py: -------------------------------------------------------------------------------- 1 | # This module shows how to access and change global variables within a package. 2 | 3 | from . import globals 4 | 5 | def test(): 6 | globals.language = "EN" 7 | globals.currency = "NZD" 8 | print(globals.language, globals.currency) 9 | 10 | -------------------------------------------------------------------------------- /chapter-7/package globals/runner.py: -------------------------------------------------------------------------------- 1 | # Simple script to load and run globalspackage.test.test() function. 2 | 3 | from globalspackage import test 4 | test.test() 5 | 6 | -------------------------------------------------------------------------------- /chapter-7/script as module gotcha/helpers.py: -------------------------------------------------------------------------------- 1 | # Helper module for the test.py script. 2 | 3 | import test 4 | 5 | def run_test(): 6 | print(test.do_something(10)) 7 | 8 | -------------------------------------------------------------------------------- /chapter-7/script as module gotcha/test.py: -------------------------------------------------------------------------------- 1 | # This program demonstrates how using a Python source file as both a module and 2 | # a script can lead to subtle errors. In this example, we define a function 3 | # which does something useful, and also have a __main__ section so we can be 4 | # executed as a script. When executed, this module imports a second module, 5 | # helpers.py, which is supposed to test out the program. The helpers.py module 6 | # then imports test.py, causing this module to be initialized twice. 7 | 8 | import helpers 9 | 10 | print("Initializing test.py") 11 | 12 | def do_something(n): 13 | return n * 2 14 | 15 | if __name__ == "__main__": 16 | helpers.run_test() 17 | 18 | -------------------------------------------------------------------------------- /chapter-7/script name gotcha/re.py: -------------------------------------------------------------------------------- 1 | # This program demonstrates how a poorly-named script can mask a module in the 2 | # python Standard Library. 3 | 4 | import re 5 | 6 | pattern = input("Regular Expression:") 7 | s = input("String:") 8 | 9 | results = re.search(pattern, s) 10 | 11 | print(results.group(), results.span()) 12 | 13 | -------------------------------------------------------------------------------- /chapter-7/stringutils/stringutils (buggy).py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def extract_numbers(s): 4 | pattern = r'[+-]?\d+(?:\.\d+)?' 5 | numbers = [] 6 | for match in re.finditer(pattern, s): 7 | number = s[match.start:match.end+1] 8 | numbers.append(number) 9 | return numbers 10 | 11 | -------------------------------------------------------------------------------- /chapter-7/stringutils/stringutils (fixed).py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def extract_numbers(s): 4 | pattern = r'[+-]?\d+(?:\.\d+)?' 5 | numbers = [] 6 | for match in re.finditer(pattern, s): 7 | number = s[match.start():match.end()] 8 | numbers.append(number) 9 | return numbers 10 | 11 | -------------------------------------------------------------------------------- /chapter-8/test-package/MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | erikwestra_test_package/__init__.py 4 | erikwestra_test_package/test.py 5 | -------------------------------------------------------------------------------- /chapter-8/test-package/README.rst: -------------------------------------------------------------------------------- 1 | erikwestra-test-package 2 | ----------------------- 3 | 4 | This is a simple test package. To use it, type:: 5 | 6 | from erikwestra_test_package import test 7 | test.run() 8 | 9 | -------------------------------------------------------------------------------- /chapter-8/test-package/erikwestra_test_package/__init__.py: -------------------------------------------------------------------------------- 1 | # Empty package initialization file. 2 | -------------------------------------------------------------------------------- /chapter-8/test-package/erikwestra_test_package/test.py: -------------------------------------------------------------------------------- 1 | # Simple test module for the "package" package. 2 | 3 | import string 4 | import random 5 | 6 | def random_name(): 7 | chars = [] 8 | for i in range(random.randrange(3, 10)): 9 | chars.append(random.choice(string.ascii_letters)) 10 | return "".join(chars) 11 | 12 | 13 | def run(): 14 | for i in range(10): 15 | print(random_name()) 16 | 17 | -------------------------------------------------------------------------------- /chapter-8/test-package/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name="erikwestra-test-package", 4 | packages=["erikwestra_test_package"], 5 | version="1.0", 6 | description="Test Package", 7 | author="Erik Westra", 8 | author_email="ewestra@gmail.com", 9 | url="https://github.com/erikwestra/test-package", 10 | download_url="https://github.com/erikwestra/test-package/tarball/1.0", 11 | keywords=["test", "python"], 12 | classifiers=[]) 13 | 14 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/quantities/README.txt: -------------------------------------------------------------------------------- 1 | About the `quantities` package 2 | ------------------------------ 3 | 4 | The `quantities` package allows you to define and work with various quantities. 5 | A quantity is a number along with an associated unit, for example: 6 | 7 | 6 inches 8 | 21.2 square kilometers 9 | 0.75 cups 10 | 11 | Using this package, you can define new quantities using the `quantities.new()` 12 | function, or convert a string into a quantity using `quantities.parse()`. For 13 | example: 14 | 15 | >>> q1 = quantities.new(6, "inch") 16 | >>> q2 = quantities.parse("21.2 square kilometers") 17 | >>> q3 = quantities.new(0.75, "cups") 18 | 19 | You can then retrieve information about these quantities by calling the 20 | appropriate functions, for example: 21 | 22 | >>> print(quantities.kind(q1)) 23 | length 24 | >>> print(quantities.value(q2)) 25 | 25 26 | >>> print(quantities.units(q3) 27 | cup 28 | 29 | You can also print a quantity directly to get a summary of its contents: 30 | 31 | >>> print(q1) 32 | 6 inch 33 | 34 | Once you have a quantity, you can convert it into a new quantity in any 35 | compatible unit. For example: 36 | 37 | >>> print(quantities.convert(q1, "mm")) 38 | 152.4 39 | >>> print(quantities.convert(q2, "hectare")) 40 | 100.0 41 | >>> print(quantities.convert(q3, "inch")) 42 | Traceback (most recent call last): 43 | File "", line 1, in 44 | TypeError: float() argument must be a string or a number 45 | ValueError: It's impossible to convert cups into inches! 46 | 47 | Finally, you can retrieve a list of the available kinds of units by calling 48 | `quantities.supported_kinds()`, and get a list of the available units of a 49 | given kind by calling `quantities.supported_units()`. For example: 50 | 51 | >>> print(quantities.supported_kinds()) 52 | ("weight", "length", "area", "volume") 53 | >>> print(quantities.supported_units("weight")) 54 | ("gram", "kilogram", "ounce", "pound") 55 | 56 | 57 | Installing and using the `quantities` package 58 | --------------------------------------------- 59 | 60 | The `quantities` package has no external dependencies, and is compatible with 61 | both Python 2 and Python 3. To install it, simply place the `quantities` 62 | package directory somewhere on your Python path. 63 | 64 | Before you can create and use quantity values, make sure you initialize the 65 | package by calling the `quantities.init()` function. This function takes a 66 | `locale` value as its parameter; the locale is used to customize the spelling 67 | of the various units, as well as allow for different unit conversion factors 68 | depending on where in the world you are. For example, in the US a cup is 69 | measured as 236.5882365 cubic centimeters, while in the rest of the world a cup 70 | is 250 cubic centimeters. 71 | 72 | The `quantities` package currently supports two locales: "us" and 73 | "international". Make sure you choose the appropriate locale when calling the 74 | `quantities.init()` function to initialize the package. 75 | 76 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/quantities/__init__.py: -------------------------------------------------------------------------------- 1 | """ __init__.py 2 | 3 | This is the package initialization file for the `quantities` package. 4 | """ 5 | from .interface import * 6 | 7 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/quantities/interface.py: -------------------------------------------------------------------------------- 1 | """ quantities.interface 2 | 3 | This module defines the public interface for the `quantities` package. 4 | """ 5 | from .units import UNITS, localize, find_unit 6 | from .quantity import Quantity 7 | 8 | ############################################################################# 9 | 10 | def init(locale): 11 | """ Initialize the 'quantities' package. 12 | 13 | 'locale' is the locale to use for calculating and parsing quantities. 14 | The following locale values are currently supported: 15 | 16 | "us" 17 | "international" 18 | """ 19 | global _locale 20 | _locale = locale 21 | 22 | 23 | def new(value, units): 24 | """ Create and return a new Quantity object. 25 | """ 26 | global _locale 27 | kind,unit = find_unit(units, _locale) 28 | if kind == None: 29 | raise ValueError("Unknown unit: {}".format(units)) 30 | 31 | return Quantity(value, localize(unit['name'], _locale)) 32 | 33 | 34 | def parse(s): 35 | """ Attempt to parse the given string into a Quantity. 36 | 37 | Raises a ValueError if the string can't be parsed into a quantity using 38 | the current locale. 39 | """ 40 | global _locale 41 | 42 | sValue,sUnits = s.split(" ", maxsplit=1) 43 | value = float(sValue) 44 | 45 | kind,unit = find_unit(sUnits, _locale) 46 | if kind == None: 47 | raise ValueError("Unknown unit: {}".format(sUnits)) 48 | 49 | return Quantity(value, localize(unit['name'], _locale)) 50 | 51 | 52 | def kind(q): 53 | """ Return the kind of unit represented by the given quantity. 54 | """ 55 | global _locale 56 | kind,unit = find_unit(q.units, _locale) 57 | return kind 58 | 59 | 60 | def value(q): 61 | """ Return the value of the given quantity. 62 | """ 63 | return q.value 64 | 65 | 66 | def units(q): 67 | """ Return the units for the given quantity. 68 | """ 69 | return q.units 70 | 71 | 72 | def convert(q, units): 73 | """ Attempt to convert the given quantity into the given units. 74 | 75 | Upon completion, we return a new quantity converted into the given 76 | units. If the quantity cannot be converted (for example, because the 77 | units are incompatible), we raise a ValueError. 78 | """ 79 | global _locale 80 | 81 | src_kind,src_units = find_unit(q.units, _locale) 82 | dst_kind,dst_units = find_unit(units, _locale) 83 | 84 | if src_kind == None: 85 | raise ValueError("Unknown units: {}".format(q.units)) 86 | if dst_kind == None: 87 | raise ValueError("Unknown units: {}".format(units)) 88 | 89 | if src_kind != dst_kind: 90 | raise ValueError("It's impossible to convert {} into {}!".format( 91 | localize(src_units['plural'], _locale), 92 | localize(dst_units['plural'], _locale))) 93 | 94 | num_units = q.value * src_units['num_units'] / dst_units['num_units'] 95 | return Quantity(num_units, localize(dst_units['name'], _locale)) 96 | 97 | 98 | def supported_kinds(): 99 | """ Return a list of the various kinds of units that we support. 100 | """ 101 | return list(UNITS.keys()) 102 | 103 | 104 | def supported_units(kind): 105 | """ Return a list of the various units of the given kind that we support. 106 | """ 107 | global _locale 108 | 109 | units = [] 110 | for unit in UNITS.get(kind, []): 111 | units.append(localize(unit['name'], _locale)) 112 | return units 113 | 114 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/quantities/quantity.py: -------------------------------------------------------------------------------- 1 | """ quantities.quantitity 2 | 3 | This module encapsulates the concept of a `quantity`. A quantity is a 4 | number along with the units that number is in. 5 | """ 6 | class Quantity(object): 7 | def __init__(self, value, units): 8 | self.value = value 9 | self.units = units 10 | 11 | def __str__(self): 12 | return "{} {}".format(self.value, self.units) 13 | 14 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/quantities/units.py: -------------------------------------------------------------------------------- 1 | """ units.py 2 | 3 | This module defins the various units supported by the `quantities` package. 4 | 5 | The 'UNITS' global holds all the defined units. This is a dictionary 6 | mapping the kind of units (ie, "weight", "length", etc) to a list of the 7 | known units of that kind. 8 | 9 | For each unit, the list item will be a dictionary with the following 10 | entries: 11 | 12 | 'abbreviation' 13 | 14 | The abbreviation for this unit. 15 | 16 | 'name' 17 | 18 | The singular name for this unit. 19 | 20 | 'plural' 21 | 22 | The plural form of the name. 23 | 24 | 'num_units' 25 | 26 | The conversion factor to apply to this unit. Note that one unit of 27 | each kind must have a conversation factor of 1, and all other units 28 | of that kind must be a multiple of that unit. 29 | 30 | Note that any of these four values can either be a single value, or a 31 | dictionary used to localize that value. If a value (ie, an abbreviation, a 32 | name, a plural name, or the number of units) is localized, then the entry 33 | will be a dictionary mapping each locale to the value to use for that 34 | locale. 35 | 36 | Locales are used to customize the names and abbreviations used for a unit 37 | depending on where in the world the user is. They are also use to 38 | distingush between US measurements and international measurements, in the 39 | cases where they differ. The following locales are currently supported: 40 | 41 | us 42 | international 43 | """ 44 | 45 | UNITS = {} 46 | 47 | ############################################################################# 48 | 49 | # Helper functions: 50 | 51 | def by_locale(value_for_us, value_for_international): 52 | """ Return a dictionary mapping "us" and "international" to the two values. 53 | 54 | This is used to create locale-specific values within our UNITS. 55 | """ 56 | return {"us" : value_for_us, 57 | "international" : value_for_international} 58 | 59 | 60 | def unit(*args): 61 | """ Create a unit dictionary. 62 | 63 | Call with (abbreviation, name, units) or (abbreviation, name, plural 64 | name, units). If the plural name is not given, it is calculated by 65 | adding "s" to the singular name. Note that the abbreviation, name, 66 | plural name (if given) and units can all be either a value or a 67 | dictionary mapping locales to values. 68 | """ 69 | if len(args) == 3: 70 | abbreviation = args[0] 71 | name = args[1] 72 | 73 | if isinstance(name, dict): 74 | plural = {} 75 | for key,value in name.items(): 76 | plural[key] = value + "s" 77 | else: 78 | plural = name + "s" 79 | 80 | num_units = args[2] 81 | elif len(args) == 4: 82 | abbreviation = args[0] 83 | name = args[1] 84 | plural = args[2] 85 | num_units = args[3] 86 | else: 87 | raise RuntimeError("Bad arguments to unit(): {}".format(args)) 88 | 89 | return {'abbreviation' : abbreviation, 90 | 'name' : name, 91 | 'plural' : plural, 92 | 'num_units' : num_units} 93 | 94 | 95 | def units(kind, *units_to_add): 96 | """ Add a list of units to the UNITS global. 97 | 98 | All the added units will be of given kind. 99 | """ 100 | if kind not in UNITS: 101 | UNITS[kind] = [] 102 | 103 | for unit in units_to_add: 104 | UNITS[kind].append(unit) 105 | 106 | ############################################################################# 107 | 108 | # Units of weight: 109 | 110 | units("weight", 111 | unit("g", "gram", 1), 112 | unit("kg", "kilogram", 1000), 113 | unit("oz", "ounce", 28.349523125), 114 | unit("lb", "pound", 453.59237)) 115 | 116 | # Units of length: 117 | 118 | units("length", 119 | unit("mm", by_locale("millimeter", "millimetre"), 0.1), 120 | unit("cm", by_locale("centimeter", "centimetre"), 1), 121 | unit("m", by_locale("meter", "metre"), 100), 122 | unit("km", by_locale("kilometer", "kilometre"), 100000), 123 | unit("in", "inch", "inches", 2.54), 124 | unit("ft", "foot", "feet", 30.48), 125 | unit("yd", "yard", 91.44), 126 | unit("mi", "mile", 160934.4)) 127 | 128 | # Units of area: 129 | 130 | units("area", 131 | unit("sq cm", by_locale("square centimeter", 132 | "square centimetre"), 0.0001), 133 | unit("sq m", by_locale("square meter", 134 | "square metre"), 1), 135 | unit("ha", "hectare", 10000), 136 | unit("sq km", by_locale("square kilometer", 137 | "square kilometre"), 1000000), 138 | unit("sq in", "square inch", "square inches", 0.00064516), 139 | unit("sq ft", "square foot", "square feet", 0.09290304), 140 | unit("sq mi", "square mile", 2589988.110336), 141 | unit("a", "acre", 4046.8564224)) 142 | 143 | # Units of volume: 144 | 145 | units("volume", 146 | unit("cc", by_locale("cubic centimeter", 147 | "cubic centimetre"), 1), 148 | unit("l", by_locale("liter", 149 | "litre"), 1000), 150 | unit("ml", by_locale("milliliter", 151 | "millilitre"), 1), 152 | unit("c", "cup", by_locale(236.5882365, 250)), 153 | unit("dsp", "dessert spoon", by_locale(9.857843187066, 11.8387809397)), 154 | unit("tsp", "teaspoon", by_locale(4.92892159375, 5.0)), 155 | unit("tbsp", "tablespoon", by_locale(14.78676478125, 15.0)), 156 | unit("cu ft", "cubic foot", "cubic feet", 28316.846592), 157 | unit("cu in", "cubic inch", "cubic inches", 16.387064), 158 | unit("cu yd", "cubic yard", 764554.857984), 159 | unit("fl oz", "fluid ounce", by_locale(29.5735295625, 28.4130625)), 160 | unit("gal", "gallon", by_locale(3785.411784, 4546.09)), 161 | unit("pt", "pint", by_locale(473.176473, 568.26125)), 162 | unit("qt", "quart", by_locale(946.352946, 1136.5225))) 163 | 164 | ############################################################################# 165 | 166 | def localize(value, locale): 167 | """ Return the value appropriate for the current locale. 168 | 169 | This is used to retrieve the appropriate localized version of a value 170 | defined within a unit's dictionary. 171 | 172 | If 'value' is a dictionary, we assume it is a mapping of locale names 173 | to values, so we select the appropriate dictionary entry based on the 174 | locale. Otherwise, we return 'value' directly, as the value hasn't 175 | been localized. 176 | """ 177 | if isinstance(value, dict): 178 | return value.get(locale) 179 | else: 180 | return value 181 | 182 | 183 | def find_unit(s, locale): 184 | """ Find the unit with the given name or abbrevation. 185 | 186 | We search through the UNITS dictionary for any unit which has a 187 | (possibly localized) singular or plural name or abbreviation matching 188 | the string 's'. If we find one, we return a (kind, unit) tuple, where 189 | 'kind' is the kind of unit and 'unit' is the dictionary entry for 190 | this unit. 191 | 192 | If we can't find any unit with that name or abbreviation, we return 193 | (None, None). 194 | """ 195 | s = s.lower() 196 | for kind in UNITS.keys(): 197 | for unit in UNITS[kind]: 198 | if (s == localize(unit['abbreviation'], locale).lower() or 199 | s == localize(unit['name'], locale).lower() or 200 | s == localize(unit['plural'], locale).lower()): 201 | # Success! 202 | return (kind, unit) 203 | 204 | return (None, None) # Not found. 205 | 206 | -------------------------------------------------------------------------------- /chapter-8/test_quantities/test_quantities.py: -------------------------------------------------------------------------------- 1 | # test_quantities.py 2 | # 3 | # This Python script performs various unit tests on the `quantities` package. 4 | 5 | import unittest 6 | import quantities 7 | 8 | class TestQuantities(unittest.TestCase): 9 | def setUp(self): 10 | quantities.init("us") 11 | 12 | def test_new(self): 13 | q = quantities.new(12, "km") 14 | self.assertEqual(quantities.value(q), 12) 15 | self.assertEqual(quantities.units(q), "kilometer") 16 | 17 | def test_convert(self): 18 | q1 = quantities.new(12, "km") 19 | q2 = quantities.convert(q1, "m") 20 | self.assertEqual(quantities.value(q2), 12000) 21 | self.assertEqual(quantities.units(q2), "meter") 22 | 23 | q = quantities.new(12, "km") 24 | with self.assertRaises(ValueError): 25 | quantities.convert(q, "kg") 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /chapter-9/counter-ranges/README.rst: -------------------------------------------------------------------------------- 1 | About the ``counter`` package 2 | ----------------------------- 3 | 4 | ``counter`` is a package designed to make it easy to keep track of the number 5 | of times some event or object occurs. Using this package, you **reset** the 6 | counter, **add** the various values to the counter, and then retrieve the 7 | calculated **totals** to see how often each value occurred. 8 | 9 | For example, imagine that you wanted to keep a count of the number of cars of 10 | each color were observed in a given timeframe. You would start by making the 11 | following call:: 12 | 13 | counter.reset() 14 | 15 | Then when you identify a car of a given color, you would make the following 16 | call:: 17 | 18 | counter.add(color) 19 | 20 | Finally, once the time period is over, you would obtain the various colours and 21 | how often they occurred in the following way:: 22 | 23 | for color,num_occurrences in counter.totals(): 24 | print(color, num_occurrences) 25 | 26 | The counter can then be reset to start counting another set of values. 27 | -------------------------------------------------------------------------------- /chapter-9/counter-ranges/counter/__init__.py: -------------------------------------------------------------------------------- 1 | # This is the package initialization file for the "counter" package. 2 | 3 | from .interface import * 4 | 5 | -------------------------------------------------------------------------------- /chapter-9/counter-ranges/counter/interface.py: -------------------------------------------------------------------------------- 1 | # This module implements the public interface to the "counter" package. 2 | 3 | def reset(ranges=None): 4 | """ Reset our counter. 5 | 6 | If 'ranges' is supplied, the given list of values will be used as the 7 | start and end of each range of values. In this case, the totals will 8 | be calculated based on a range of values rather than individual values. 9 | 10 | This should be called before we start counting. 11 | """ 12 | global _ranges 13 | global _counts 14 | 15 | _ranges = ranges 16 | _counts = {} # If _ranges is None, maps value to number of occurrences. 17 | # Otherwise, maps (min_value,max_value) to number of 18 | # occurrences. 19 | 20 | 21 | def add(value): 22 | """ Add the given value to our counter. 23 | """ 24 | global _ranges 25 | global _counts 26 | 27 | if _ranges == None: 28 | key = value 29 | else: 30 | key = None 31 | for i in range(len(_ranges)-1): 32 | if value >= _ranges[i] and value < _ranges[i+1]: 33 | key = (_ranges[i], _ranges[i+1]) 34 | break 35 | if key == None: 36 | raise RuntimeError("Value out of range: {}".format(value)) 37 | 38 | try: 39 | _counts[key] += 1 40 | except KeyError: 41 | _counts[key] = 1 42 | 43 | 44 | def totals(): 45 | """ Return the number of times each value has occurred. 46 | 47 | If we are currently counting ranges of values, we return a list of 48 | (min_value, max_value, num_occurrences) tuples, one for each range. 49 | Otherwise, we return a list of (value, num_occurrences) tuples, one for 50 | each unique value included in the count. 51 | """ 52 | global _ranges 53 | global _counts 54 | 55 | if _ranges != None: 56 | results = [] 57 | for i in range(len(_ranges)-1): 58 | min_value = _ranges[i] 59 | max_value = _ranges[i+1] 60 | num_occurrences = _counts.get((min_value, max_value), 0) 61 | results.append((min_value, max_value, num_occurrences)) 62 | return results 63 | else: 64 | results = [] 65 | for value in sorted(_counts.keys()): 66 | results.append((value, _counts[value])) 67 | return results 68 | 69 | -------------------------------------------------------------------------------- /chapter-9/counter-ranges/tests.py: -------------------------------------------------------------------------------- 1 | # This module implements various unit tests for the ``counter`` package. 2 | 3 | import unittest 4 | 5 | import counter 6 | 7 | class CounterTestCase(unittest.TestCase): 8 | """ Unit tests for the ``counter`` package. 9 | """ 10 | def test_counter_totals(self): 11 | counter.reset() 12 | counter.add(1) 13 | counter.add(2) 14 | counter.add(3) 15 | counter.add(1) 16 | self.assertEqual(counter.totals(), [(1, 2), (2, 1), (3, 1)]) 17 | 18 | def test_counter_reset(self): 19 | counter.reset() 20 | counter.add(1) 21 | counter.reset() 22 | counter.add(2) 23 | self.assertEqual(counter.totals(), [(2, 1)]) 24 | 25 | 26 | class RangeCounterTestCase(unittest.TestCase): 27 | """ Unit tests for the range-based features of ``counter`` package. 28 | """ 29 | def test_range_totals(self): 30 | counter.reset([0, 5, 10, 15]) 31 | counter.add(3) 32 | counter.add(9) 33 | counter.add(4.5) 34 | counter.add(12) 35 | counter.add(14.2) 36 | counter.add(8) 37 | self.assertEqual(counter.totals(), 38 | [(0, 5, 2), (5, 10, 2), (10, 15, 2)]) 39 | 40 | def test_out_of_range(self): 41 | counter.reset([0, 5, 10, 15]) 42 | with self.assertRaises(RuntimeError): 43 | counter.add(19.1) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | 49 | -------------------------------------------------------------------------------- /chapter-9/counter/README.rst: -------------------------------------------------------------------------------- 1 | About the ``counter`` package 2 | ----------------------------- 3 | 4 | ``counter`` is a package designed to make it easy to keep track of the number 5 | of times some event or object occurs. Using this package, you **reset** the 6 | counter, **add** the various values to the counter, and then retrieve the 7 | calculated **totals** to see how often each value occurred. 8 | 9 | For example, imagine that you wanted to keep a count of the number of cars of 10 | each color were observed in a given timeframe. You would start by making the 11 | following call:: 12 | 13 | counter.reset() 14 | 15 | Then when you identify a car of a given color, you would make the following 16 | call:: 17 | 18 | counter.add(color) 19 | 20 | Finally, once the time period is over, you would obtain the various colours and 21 | how often they occurred in the following way:: 22 | 23 | for color,num_occurrences in counter.totals(): 24 | print(color, num_occurrences) 25 | 26 | The counter can then be reset to start counting another set of values. 27 | -------------------------------------------------------------------------------- /chapter-9/counter/counter/__init__.py: -------------------------------------------------------------------------------- 1 | # This is the package initialization file for the "counter" package. 2 | 3 | from .interface import * 4 | 5 | -------------------------------------------------------------------------------- /chapter-9/counter/counter/interface.py: -------------------------------------------------------------------------------- 1 | # This module implements the public interface to the "counter" package. 2 | 3 | def reset(): 4 | """ Reset our counter. 5 | 6 | This should be called before we start counting. 7 | """ 8 | global _counts 9 | _counts = {} # Maps value to number of occurrences. 10 | 11 | 12 | def add(value): 13 | """ Add the given value to our counter. 14 | """ 15 | global _counts 16 | 17 | try: 18 | _counts[value] += 1 19 | except KeyError: 20 | _counts[value] = 1 21 | 22 | 23 | def totals(): 24 | """ Return the number of times each value has occurred. 25 | 26 | We return a list of (value, num_occurrences) tuples, one 27 | for each unique value included in the count. 28 | """ 29 | global _counts 30 | 31 | results = [] 32 | for value in sorted(_counts.keys()): 33 | results.append((value, _counts[value])) 34 | return results 35 | 36 | -------------------------------------------------------------------------------- /chapter-9/counter/tests.py: -------------------------------------------------------------------------------- 1 | # This module implements various unit tests for the ``counter`` package. 2 | 3 | import unittest 4 | 5 | import counter 6 | 7 | class CounterTestCase(unittest.TestCase): 8 | """ Unit tests for the ``counter`` package. 9 | """ 10 | def test_counter_totals(self): 11 | counter.reset() 12 | counter.add(1) 13 | counter.add(2) 14 | counter.add(3) 15 | counter.add(1) 16 | self.assertEqual(counter.totals(), [(1, 2), (2, 1), (3, 1)]) 17 | 18 | def test_counter_reset(self): 19 | counter.reset() 20 | counter.add(1) 21 | counter.reset() 22 | counter.add(2) 23 | self.assertEqual(counter.totals(), [(2, 1)]) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | 29 | --------------------------------------------------------------------------------