Compiled on: 2024-05-07 — printable version
In order to put everybody on the same page…
… we are going to recall some basic notions and technical aspects related to software development, namely:
(a.k.a. the shell, the terminal, the console)
The terminal is a text-based interface to the operative system (OS). Each terminal application is executing a shell program
The shell is a program that has a simple job: REPL (Read, Evaluate, Print, Loop)
- it reads a command from the user
- it evaluates the command
- it prints the result
- it loops back to step 1, unless the user explicitly asks to exit
ls
(or dir
on Windows): should list the files in the current directoryecho "Hello World"
: should print “Hello World” on the screenexit
: should close the shell (and the terminal application, if it’s the only shell)Mostly, to look hacker-ish in the eyes of normal people 😎
(just kidding)
The terminal is a powerful tool for software development
It allows developers to interact with the OS in a precise, minimal, and efficient way
Most operations in software development can, and often should, be performed from the terminal
The terminal may accept commands:
- interactively, from the user
- from a script, which is a file containing commands to be executed by a shell
Developers’ mindset:
If a person can do it manually via the shell, then a script can do it automatically
If a script can do it automatically, then that’s the way of doing it
Beware, ‘cause scripts are software too, and they require engineering:
This is not really the case of everyday programming tasks, but let’s keep this in mind
We are going to use the terminal for:
Understaing how to do stuff via the terminal is a reusable skill
We encourage you to read the following lectures from The MIT’s “Missing Semester of Your CS Education”:
bash
, zsh
, fish
, ksh
, csh
, tcsh
, etc.) used by Linux and macOS
cmd
, PowerShell
) which are different from the Unix-like ones, and from each otherWhenever working with the terminal, first thing to do is to understand which shell you are using
- if you’re on Linux, you’re probably using
bash
orzsh
- if you’re on macOS, you’re probably using
zsh
orbash
- if you’re on Windows, you’re probably using
cmd
if you opened the Command Prompt applicationPowerShell
is you opened the PowerShell applicationbash
if are using the Windows Subsystem for Linux (WSL) or Git Bash
Whenver you open a shell, the shell is “in” a directory, which is called the current working directory (CWD)
If one wants to operate on a file in a different directory…
… they have to change the CWD
cd
command (change directory)… without changing the CWD, they have to specify the path to the file
A path is a string that represents the location of a file or a directory in the file system
Beware: path-separator is different among Windows (
\
) and other OS (/
),and we only use
/
in the slides
A relative path is a path that is relative to the CWD
./file.txt
refers to a file named file.txt
in the CWD../file.txt
refers to a file named file.txt
in the parent directory of the CWD./dir/file.txt
refers to a file named file.txt
in a sub-directory of CWD, named dir
An absolute path is a path that starts from the root of the file system
/
/home/giovanni/file.txt
refers to a file named file.txt
in giovanni
’s home directory on Linux/Users/giovanni/file.txt
refers to a file named file.txt
in giovanni
’s home directory on macOSC:
, D:
, etc.)
C:\Users\giovanni\file.txt
refers file file.txt
in giovanni
’s home directory, on drive C:
D:\Data\Photos\profile.jpg
refers file profile.jpg
in the Data\Photos
directory, on drive D:
Operation | *nix | win |
---|---|---|
Print the current directory location | pwd |
echo %cd% |
Remove the file foo (does not work with directories) |
rm foo |
del foo |
Remove directory bar |
rm -r bar |
del bar |
Change disk (e.g., switch to D: ) |
n.a., single root (/ ) |
D: |
Move to the subdirectory baz |
cd baz |
cd baz |
Move to the parent directory | cd .. |
cd.. |
Move (rename) file foo to baz |
mv foo baz |
move foo baz |
Copy file foo to baz |
cp foo baz |
copy foo baz |
Create a directory named bar |
mkdir bar |
md bar |
Most commands have arguments
If you think of commands as functions, then arguments are the parameters of the function
Consider the ls
command
ls
lists the files in the CWD, as an inline listls -l
lists the files in the CWD, as a detailed listls -l /path/to/dir
lists the files in the /path/to/dir
directory, as a detailed listYou should not.
Just try to grasp the basic idea of how shells work
Just memorise that there exist a way to do X
via the shell
X
sYou will eventually memorise the syntax of most frequent commands
For the rest, you can always look up the documentation
Most commands support asking for help when one does not remeber the syntax
COMMAND_NAME --help
or COMMAND_NAME -h
mostly used on Unix-like systemsman COMMAND_NAME
mostly used on Unix-like systems (man is for “manual”)COMMAND_NAME /?
mostly used on WindowsGet-Help COMMAND_NAME
mostly used on WindowsDo not waste your memory, learn how to look up the documentation instead
Some commands are interactive
In this case we say that the command is just starting a process
There is no difference among interactive and non-interactive processes, for the shell
Upon termination, each command returns an exit code (i.e. a non-negative integer)
0
means “everything went fine”When using the shell interactively:
When programming the shell in a script:
$?
if
statement to check the exit code and act accordinglyIn the eyes of the OS, any process can be modelled as follows:
i.e. a black box
stdin
)stdout
)stderr
)more channels may be opened by the process, e.g. when reading / writing files
a stream is an unlimited sequence of bytes (or characters)
Most commonly, for interactive processes, the situation is as follows:
nano
command
nano
is a simple, interactive, text editor for the terminal
Open a shell, and run the following command
nano myfile.txt
This should transform the terminal into a text editor, editing the file myfile.txt
Hello World
Then, press Ctrl+O to save the file
Then, press Ctrl+X to exit the editor
You should be back to the shell, and the file myfile.txt
should have been created
ls
commandPython is a programming language, namely, the reference programming language we use in the course
We will operate Python stuff via the terminal, using the python
command
The command’s behaviour is very different depending on which and how many arguments are passed:
python
with no arguments starts an interactive Python shell
python FILENAME
starts a Python process that executes the Python code in the file FILENAME
python -m MODULE
starts a Python process that executes the module named MODULE
python -c "CODE"
starts a Python process that executes the Python code in the string "CODE"
python -c "print('Hello World')"
Use python --help
to inspect the help of the python
command, and see all the options
When using Python, always remember to check the version of the Python interpreter you are using
python --version
or python -V
(in Python)
Let’s say we are going to build a simple calculator app, in Python
Using Kivy for the GUI, we may easily build the following app:
The source code for such application is available here:
https://github.com/unibo-dtm-se/compact-calculator
ls -la
command)
.python-version
(hidden on Unix) textual declaration of the Python version required by the applicationcalculator.py
: the source code of the applicationrequirements.txt
: a file containing the dependencies of the applicationREADME.md
: a file containing the some notes about of the applicationNotice that the calculator.py
file is a Python script that contains only 46 lines of code
Have a look to the source code of the calculator.py
file
Let’s try to run the application
python calculator.py
Traceback (most recent call last):
File "/path/to/your/directory/calculator.py", line 1, in <module>
from kivy.app import App
ModuleNotFoundError: No module named 'kivy'
The issue here is that our application depends on some third-party library, namely Kivy
The solution is pretty simple: let’s install the missing dependency
pip
command, which is the Python package manager
pip install kivy
After the installation, we can try to run the application again
python calculator.py
this time, the application should start, and you should see the calculator Window
play a bit with the application
either in the terminal (ls -la
), or in the GUI, you may notice a new sub-directory named __pycache__
calculator.cpython-3XXX
.pyc
(or similar) in it
the notion of library
the notion of dependency
the notion of runtime
the notion of package manager
the notion of compilation
A program is a set of instructions that a computer can execute
Computers nowadays follow the Von Neumann architecture
x86
, amd64
, arm
, etc.)
— different architectures have different instruction setsProgramming languages are meant to be understood by humans (not by computers)
To make a computer understand a program, the program must be translated into machine code
The translation procedure is hardware-dependent, and it may vary among OS
There are two main ways to translate a program into machine code:
The result is the same (the program is executed, i.e. the computer follows the instructions)
Compilation implies:
Interpretation implies:
Historically, programming languages have been categorised as either compiled or interpreted
Nowadays, the distinction is blurred
As always, mixing opposite approaches may lead to the best of both worlds
The idea of saving a hard-to-achieve result for later re-use is called caching
The CPython interpreter (i.e. the reference Python implementaton) adopts a strategy of this sort
Basically no programmer ever writes an entire application from scratch
One key principle in SE has always been:
Don’t reinvent the wheel
SE is essentially about how to write good code, which works well, and can be reused in the future
Collections of reusable code are called libraries
All programming languages have a standard library…
math
module in Python, the java.util
package in Java, etc.… plus some mechanism to install and import third-party libraries
pip
command is used to install third-party librariesimport
statement is used to import libraries in the script
The consequences of this “library” idea are manifold
The runtime is the environment in which a piece of software is executed
Runtime of a program $\approx$ jargon for “the set of libraries actually available for that program at run-time”
Developers exploit libraries produced by others to avoid wasting time reinventing the wheel
The reasoning is more or less as follows:
A dependency among some software $S$ and some other software $L$
occurs when $S$ requires $L$ to work
Some definitions related to the notion of dependency:
Transitive dependency: if $S$ depends on $L$, and $L$ depends on $M$, then $S$ transitively depends on $M$
Dependency graph of a software $S$: the graph spawned by all the dependencies (direct or transitive) depedencies of $S$
calculator.py
├── Python 3.11.7
└── kivy 2.3.0
├── docutils *
├── kivy-deps-angle >=0.4.0,<0.5.0
├── kivy-deps-glew >=0.3.1,<0.4.0
├── kivy-deps-sdl2 >=0.7.0,<0.8.0
├── kivy-garden >=0.1.4
│ └── requests *
│ ├── certifi >=2017.4.17
│ ├── charset-normalizer >=2,<4
│ ├── idna >=2.5,<4
│ └── urllib3 >=1.21.1,<3
├── pygments *
└── pypiwin32 *
└── pywin32 >=223
To support the extension of runtimes, and therefore the addition of dependencies…
… most programming languages come with 2 related tools:
Packgage $\approx$ a piece of software with a name and a version, and a fixed structure eases installation and reuse
Package managers commonly support specifying from which repository a dependency should be installed
In the Python world:
pip
command is the default package manager, and it is tightly integrated with PyPI
pip install NAME
installs the last version of the package NAME
from PyPIIt is a good practice to document which dependencies a software relies upon
It is even a better practice to automate the installation of dependencies
Other than package managers and repositories, automation requires dependency declaration
In the Python world, there are several conventions for dependency declaration
requirements.txt
file
NAME==VERSION
pip install -r requirements.txt
command installs all depndencis in the file.python-version
file
pyenv
install
command can install corresponding version of PythonThe Python world is not the only one where package managers and package repositories are used
Most programming languages have their own package manager and package repository
In the Linux world, package managers/repositories are used at the OS level too
On MacOS, the Homebrew package manager is widely used (not shipped with the OS)
On Windows, one can use chocolatey or scoop as package managers (not shipped with the OS)
Notice that private companies may have their own private package repositories
Also notice that some private companies may want to publish their software to public repositories
In all such cases, you may interpret the release and deployment activities as follows:
Release: the activity of making a particular version of some package available on a package manager
Deployment: the process of installing a particular version of a system onto the production environment
Let’s delve into the actual code of the calculator application
(focus on the comments)
# Import a bunch of stuff from the Kivy library, used below
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
# Matrix of button names and their layout in the GUI
BUTTONS_NAMES = [
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['.', '0', '=', '+'],
]
# Calculator *class*: template for all sorts of calculators. this is a particular case of App (i.e. a window, in Kivy)
class CalculatorApp(App):
# Method to build the GUI of the calculator, accordinging to Kivy's conventions
def build(self):
# Definition & initialisation of the "expression" field of the calculator.
# This fields stores a string, representing the expression to be evaluated when "=" is pressed
self.expression = ""
# Let's create a layout, i.e. a virtual container of the visual components the GUI.
# The grid shall dispose components vertically (top to bottom), i.e. it contains *rows* of components
grid = BoxLayout(orientation='vertical')
# Let's create a label, which will serve as the display of the calculator
self._display = Label(text='0', font_size=24, size_hint=(1, 0.75))
# Let's add the label to the grid, as the first row
grid.add_widget(self._display)
# For each *list of* button names in the matrix of button names...
for button_names_row in BUTTONS_NAMES:
# ... let's create another virtual container for a *row* for components
grid_row = BoxLayout()
# ... then, for each button name in the list of button names...
for button_name in button_names_row:
# ... let's create a button, having the button name as text
# (the button is configured to call method on_button_press when pressed)
button = Button(text=button_name, font_size=24, on_press=self.on_button_press)
# ... and let's add the button to the row
grid_row.add_widget(button)
# ... and let's add the row to the grid
grid.add_widget(grid_row)
# Finally, let's return the grid, what will be showed in the window
return grid
# Method to be called when a button is pressed
def on_button_press(self, button):
# If the button is the "=" button
if button.text == '=':
# Try to...
try:
# ... evaluate the expresion *as a Python expression*, convert the result to a string,
# and show that string on the calculator display
self._display.text = str(eval(self.expression))
# If an error occurs in doing the above (e.g. wrong expression)
except SyntaxError:
# ... set the display to "Error"
self._display.text = 'Error'
# Reset the calculator's expression
self.expression = ""
# If the button is any other button
else:
# Append the button's text to the calculator's expression
self.expression += instance.text
# Show the calculator's expression on the display
self._display.text = self.expression
# If the script is executed as a standalone program (i.e. not imported as a module)
if __name__ == '__main__':
# Let's create a new calculator application, and run it
CalculatorApp().run()
calculator.py
, with a single class CalculatorApp
The system is what the user sees (i.e. the view)
The code is not modular: view, controller, and model are mixed together
Requirements may change, e.g.: customers may ask for:
It may be hard to change the model, without breaking the view or the controller
It may be hard to test the model, without testing the view or the controller
The application is, and will always only be, a desktop application
A good way to decompose code is to follow the Model-View-Controller (MVC) pattern
Different portions of the code are responsible for different aspects of the application
The desktop application is just one of many options
The same model could be attached to several different views, via as many controllers
The code of each portion could be developed and tested independently
A bug in one portion of the code may be noticed before it affects the others
It allows managers, developers, and users to reason about
The idea of decomposing code into smaller, simpler units is pervasive in SE
Generally speaking, this idea is called modularity
Each module should have
Two important notions here:
Programming languages are plenty of mechanisms to support modularity, at different levels
Functions are the most basic form of modularity in Python
def sum(a, b):
return a + b
Classes are a way to encapsulate both data and functions into a single unit
class Pair:
def __init__(self, fst, snd):
self.first = fst
self.second = snd
def sum(self):
return self.first + self.second
Modules are a way to encapsulate related functions and classes into a single unit
.py
file containing Python code# file: path/to/pair.py
class Pair:
def __init__(self, fst, snd):
self.first = fst
self.second = snd
def sum(self):
return self.first + self.second
p1 = Pair(1, 2)
p2 = Pair(3, 4)
Packages are a way to organize related modules in a hierarchical way
# another Python file in the same root directory
from path.to.pair import p1, p2
x1 = p1.sum() # 3
x2 = p2.sum() # 7
Beware: Other programming languages may have similar mechanisms, but with different names, or they may have different mechanisms to achieve the same goals, or they may have a subset of the mechanisms available in Python
As code is decomposed into smaller and smaller units…
It is important to organize the code in a meaningful way
Recall that code is meant to be read by humans, and that
As the software grows, a good organization of the code mitigates the following issues:
Let’s decompose the calculator application in such a way that
python -m calculator.ui.gui
python -m calculator.ui.cli "1 + 1"
2
python -m calculator.ui.cli "2*3"
6
The source code for such application is available here:
https://github.com/unibo-dtm-se/modular-calculator
tree
command)
modular-calculator/ # project root directory
├── calculator/ # the main package of the project (full name: calculator)
│ ├── __init__.py # the package initialization content
│ └── ui/ # the user interface sub-package (full name: calculator.ui)
│ ├── cli.py # the command-line interface module (full name: calculator.ui.cli)
│ └── gui.py # the graphical user interface module (full name: calculator.ui.gui)
├── .python-version # the Python version required by the project
├── README.md # a file containing the some _notes_ about of the application
├── requirements.txt # a file declaring the _dependencies_ of the application
└── .vscode # a directory containing the VS Code configuration
└── launch.json # a file containing the VS Code launch configuration
Let’s try to run the GUI application
python -m calculator.ui.gui
the application should start, and you should see the calculator Window
Let’s try to run the CLI application
python -m calculator.ui.cli
you should get a hint about how to use the CLI application
Let’s analyze how the code has been decomposed
calculator
module
calculator/__init__.py
filecalculator.ui.gui
module
calculator/ui/gui.py
filecalculator.ui.cli
module
calculator/ui/cli.py
fileCode from calculator/__init__.py
:
Number = int | float
class Calculator:
def __init__(self):
self.expression = ""
def _ensure_is_digit(self, value: int | str):
if isinstance(value, str):
value = int(value)
if value not in range(10):
raise ValueError("Value must a digit in [0, 9]: " + value)
return value
def _append(self, value):
self.expression += str(value)
def digit(self, value: int | str):
value = self._ensure_is_digit(value)
self._append(value)
def plus(self):
self._append("+")
def minus(self):
self._append("-")
def multiply(self):
self._append("*")
def divide(self):
self._append("/")
def dot(self):
self._append(".")
def compute_result(self) -> Number:
try:
result = eval(self.expression)
if isinstance(result, Number):
self.expression = str(result)
return result
else:
raise ValueError("Result is not a number: " + str(result))
except SyntaxError as e:
expression = self.expression
self.expression = ""
raise ValueError("Invalid expression: " + expression) from e
Class diagram:
Usage example:
from calculator import Calculator
c = Calculator() # create a new calculator
c.digit(1) # append digit 1
c.plus() # append the plus sign
c.digit(2) # append digit 2
print(c.expression) # "1+2"
print(c.compute_result()) # 3
print(c.expression) # ""
Code from calculator/ui/gui.py
:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from calculator import Calculator # notice this line
BUTTONS_NAMES = [
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['.', '0', '=', '+'],
]
class CalculatorApp(App):
def build(self):
self._calc = Calculator() # new entry here
grid = BoxLayout(orientation='vertical')
self._display = Label(text="0", font_size=24, size_hint=(1, 0.75))
grid.add_widget(self._display)
for button_names_row in BUTTONS_NAMES:
grid_row = BoxLayout()
for button_name in button_names_row:
button = Button(text=button_name, font_size=24, on_press=self.on_button_press)
grid_row.add_widget(button)
grid.add_widget(grid_row)
return grid
def on_button_press(self, button): # completely rewritten
match button.text:
case "=":
try:
result = self._calc.compute_result()
self._display.text = str(result)
except ValueError as e:
self._display.text = "Error"
case "+":
self._calc.plus()
self._display.text = self._calc.expression
case "-":
self._calc.minus()
self._display.text = self._calc.expression
case "*":
self._calc.multiply()
self._display.text = self._calc.expression
case "/":
self._calc.divide()
self._display.text = self._calc.expression
case ".":
self._calc.dot()
self._display.text = self._calc.expression
case _:
self._calc.digit(button.text)
self._display.text = self._calc.expression
if __name__ == '__main__':
CalculatorApp().run()
Content of calculator/ui/cli.py
:
from calculator import Calculator
import sys
class CalculatorCLI:
def __init__(self, args):
self._args = args
self._calc = Calculator()
def run(self):
if not self._args:
print("Usage: python -m calculator.ui.cli <expression>")
return
self._calc.expression = " ".join(self._args)
try:
result = self._calc.compute_result()
print(result)
except ValueError as e:
print(e)
if __name__ == '__main__':
CalculatorCLI(sys.argv[1:]).run()
(including dependencies from Python and Kivy)
the notion of software design
the notion of interface, and, most notably:
the notion of project structure
It’s important to organize files and directories in software projects in a clear way
For Python projects, the ultimate project structure will be clear along the way in the course…
… for the time being, let’s stick to the following project structure:
root-directory/
├── main_package/
│ ├── __init__.py
│ ├── sub_module.py
│ └── sub_package/
│ ├── __init__.py
│ └── sub_sub_module.py
├── .python-version
├── README.md
└── requirements.txt
Further files/directories will pop-up in the next lectures
An application’s UI is any means by which human users interact with the application
Regardless of the appearance, the functioning of the UI can be summarised as follows:
Engineering the UI is a complex task, as it involves many aspects of human-computer interaction (HCI)…
… and, of course, elciting the admissible inputs for the UI:
Disclaimer:
While UI is very important, and engineering UI is part of SE, it is not the only part of SE. It is far more important in SE to take care about the API
The rationale is as follows:
The application programming interface (API) of a software is:
- the set of functionalities the software provides to developers for building other software on top of it
- a (semi-)formal specification of 1, including, for each functionality:
- the name by which it is known / referenced
- the formal parameters it accepts as input
- the return values or effects it produces as outcome
- the pre-conditions and post-conditions it satisfies/establishes
Recall that software is often built on top of other software, in a layered way:
You can think of a piece of software as a provider of services to their users
At the same time, a piece of software could be a consumer of services from other software
The API is the contract between the provider and the consumer of services
The provider of the services knows how to provide the services
The consumer of the services knows what the services do, and how to exploit them
A change in the contract would affect both sides
The API of a Python project consists of
- “Public” means “intended for external usage”
- i.e. “to be used by other developers (other than the ones who wrote the current code)”
- the opposite of “public” is “private”
In Python, everything is public by convention
_
)__example__
) are magic methods
Beware because Python adopts the “consenting adults” convention
Another way to think about private stuff in a software project is:
private stuff may change without notice, in any moment
Class diagrams are a good way to represent the API (as well as the structure) of a software project
I use PlantUML to draw class diagrams
Get used to:
- draw class diagrams for your software projects
- read class diagrams of other software projects
- reverse-engineer (or just imagine) the class diagrams of software projects you use
(spoiler: both)
A command line application can be seen as:
Recall that a command-line application is just one more command for the shell
The API of a command line application is a description of
Most commonly, the API of a command line application can be inspected via the --help
argument
Depending on which and how many interfaces a software project exposes…
… the aim of that software project is different
For instace:
Further categories may appear along the way in the course:
Notice that the aforementioned categories are not mutually exclusive
For instance, in order for a software project to support several UI…
We recommend to reason about the aim of the software since the very beginning
After understanding the requirements, and the domain at hand…
… software architects will reason about the design of the software
the result of such reasoning is a model of the software
Let’s consider the calculator application
str
in Python)Calculator
classCalculator
classint
or float
)How should those concepts be related to each other?
Calculator
object is composed by one (and only one) Expression
object
How should the Calculator
class behave?
Calculator
class are responsible updating the Expression
Expression
into a Result
Expression
to an empty oneAll exercises involve either analysing or editing the code of the (modular) calculator application
The calculator has a bug: it does not properly handle division by zero
Is this a presentation, a modelling, or an implementation issue?
play with the software to understand the bug
solve the bug (by doing some minimal change in the code)
The calculator GUI is missing a feature: it gives no way to the user to clear the current expression
We totally missed that functionality when designing the model and the GUI
While minimising changes in the code: