Compare commits

..

19 Commits

Author SHA1 Message Date
4ecaa1a216 Add final test material 2022-12-20 21:49:28 -04:00
fcef1277f3 Merge remote-tracking branch 'origin/master' 2022-12-07 23:49:32 -04:00
8507082995 Assignment 6 skeleton 2022-12-07 23:49:22 -04:00
e091a825ce Add quiz 3 content, and quiz 3 practice 2022-11-23 10:09:24 -04:00
8501f2f473 Update tests 2022-11-18 16:30:31 -04:00
bfdc5dc93b Add assignment 5 content 2022-11-18 16:28:38 -04:00
d81425a781 Remove virtual env
I only need the venv on 2 machines and its easier just to have a requirements.txt
2022-11-16 10:00:27 -04:00
62f9f2677d Add lab 17 stuff 2022-11-16 09:59:43 -04:00
95918841b0 Add lab 16 stuff 2022-11-16 09:59:08 -04:00
419e67f80a Add tests 2022-11-04 17:53:24 -03:00
38135762f6 Add exploding bunny rabbit 2022-11-04 17:53:17 -03:00
5c03647a4a Add assignment 4 2022-11-04 15:07:21 -03:00
3ee1155e1f Merge remote-tracking branch 'origin/master' 2022-11-04 15:03:31 -03:00
efb36f83fc Mark executables binary 2022-11-04 15:02:58 -03:00
d28bd6ed1c Remove shebang 2022-11-02 10:02:34 -03:00
45c5b94e63 Add some lab 15 contents 2022-11-02 10:02:15 -03:00
0f1e8d5e36 Add run configs for lab 14 2022-11-02 09:43:37 -03:00
57405f5aac Fix python venv 2022-11-02 09:15:23 -03:00
8500f351fc Add lab 15 skeleton 2022-11-02 09:15:03 -03:00
981 changed files with 4574 additions and 287882 deletions

1
.gitattributes vendored
View File

@ -3,3 +3,4 @@
*.png binary
*.jpg binary
*.exe binary

178
assignments/A4/life.js Normal file
View File

@ -0,0 +1,178 @@
// ES2015 classes based on https://eloquentjavascript.net/2nd_edition/07_elife.html
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
plus(other) {
return new Vector(this.x + other.x, this.y + other.y);
};
};
class Grid {
constructor (width, height) {
this.space = new Array(width * height);
this.width = width;
this.height = height;
};
isInside(vector) {
return vector.x >= 0 && vector.x < this.width &&
vector.y >= 0 && vector.y < this.height;
};
get(vector) {
return this.space[vector.x + this.width * vector.y];
};
set(vector, value) {
this.space[vector.x + this.width * vector.y] = value;
};
forEach(f, context) {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
let value = this.space[x + y * this.width];
if (value != null)
f.call(context, value, new Vector(x, y));
}
}
};
}
let directions = {
"n": new Vector( 0, -1),
"ne": new Vector( 1, -1),
"e": new Vector( 1, 0),
"se": new Vector( 1, 1),
"s": new Vector( 0, 1),
"sw": new Vector(-1, 1),
"w": new Vector(-1, 0),
"nw": new Vector(-1, -1)
};
function randomElement(array) {
return array[Math.floor(Math.random() * array.length)];
}
let directionNames = "n ne e se s sw w nw".split(" ");
function BouncingCritter() {
this.direction = randomElement(directionNames);
};
BouncingCritter.prototype.act = function(view) {
if (view.look(this.direction) != " ")
this.direction = view.find(" ") || "s";
return {type: "move", direction: this.direction};
};
class View {
constructor(world, vector) {
this.world = world;
this.vector = vector;
}
look(dir) {
let target = this.vector.plus(directions[dir]);
if (this.world.grid.isInside(target))
return charFromElement(this.world.grid.get(target));
else
return "#";
}
findAll(ch) {
let found = [];
for (let dir in directions)
if (this.look(dir) == ch)
found.push(dir);
return found;
}
find(ch) {
let found = this.findAll(ch);
if (found.length == 0) return null;
return randomElement(found);
}
}
class World {
constructor(map, legend) {
let grid = new Grid(map[0].length, map.length);
this.grid = grid;
this.legend = legend;
map.forEach(function(line, y) {
for (let x = 0; x < line.length; x++)
grid.set(new Vector(x, y),
World.elementFromChar(legend, line[x]));
});
}
static elementFromChar(legend, ch) {
if (ch == " ")
return null;
let element = new legend[ch]();
element.originChar = ch;
return element;
}
toString() {
let output = "";
for (let y = 0; y < this.grid.height; y++) {
for (let x = 0; x < this.grid.width; x++) {
let element = this.grid.get(new Vector(x, y));
output += charFromElement(element);
}
output += "\n";
}
return output;
}
turn () {
let acted = [];
this.grid.forEach(function(critter, vector) {
if (critter.act && acted.indexOf(critter) == -1) {
acted.push(critter);
this.letAct(critter, vector);
}
}, this);
}
letAct(critter, vector) {
let action = critter.act(new View(this, vector));
if (action && action.type == "move") {
let dest = this.checkDestination(action, vector);
if (dest && this.grid.get(dest) == null) {
this.grid.set(vector, null);
this.grid.set(dest, critter);
}
}
}
checkDestination(action, vector) {
if (directions.hasOwnProperty(action.direction)) {
let dest = vector.plus(directions[action.direction]);
if (this.grid.isInside(dest))
return dest;
}
return undefined;
}
};
function charFromElement(element) {
if (element == null)
return " ";
else
return element.originChar;
}
function Wall() {};
exports.BouncingCritter=BouncingCritter;
exports.Grid=Grid;
exports.Wall=Wall;
exports.World=World;
exports.Vector=Vector;
exports.View=View;

122
assignments/A4/moarlife.js Normal file
View File

@ -0,0 +1,122 @@
let life=require("./life.js");
let View=life.View;
let actionTypes = Object.create(null);
actionTypes.grow = function(critter) {
critter.energy += 0.5;
return true;
};
actionTypes.move = function(critter, vector, action) {
let dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 1 ||
this.grid.get(dest) != null)
return false;
critter.energy -= 1;
this.grid.set(vector, null);
this.grid.set(dest, critter);
return true;
};
actionTypes.eat = function(critter, vector, action) {
let dest = this.checkDestination(action, vector);
let atDest = dest != null && this.grid.get(dest);
if (!atDest || atDest.energy == null)
return false;
critter.energy += atDest.energy;
this.grid.set(dest, null);
return true;
};
actionTypes.reproduce = function(critter, vector, action) {
let baby = life.World.elementFromChar(this.legend,
critter.originChar);
let dest = this.checkDestination(action, vector);
if (dest == null ||
critter.energy <= 2 * baby.energy ||
this.grid.get(dest) != null)
return false;
critter.energy -= 2 * baby.energy;
this.grid.set(dest, baby);
return true;
};
actionTypes.die = function(critter, action) {
}
class LifelikeWorld extends life.World {
constructor(map,legend){
super(map,legend);
}
letAct(critter, vector) {
let action = critter.act(new View(this, vector));
let handled = action &&
action.type in actionTypes &&
actionTypes[action.type].call(this, critter,
vector, action);
if (!handled) {
critter.energy -= 0.2;
if (critter.energy <= 0)
this.grid.set(vector, null);
}
};
}
class Plant {
constructor() {
this.energy = 3 + Math.random() * 4;
}
act(view) {
if (this.energy > 15) {
let space = view.find(" ");
if (space)
return {type: "reproduce", direction: space};
}
if (this.energy < 20)
return {type: "grow"};
};
}
class PlantEater{
constructor () {
this.energy = 20;
}
act(view) {
let space = view.find(" ");
if (this.energy > 60 && space)
return {type: "reproduce", direction: space};
let plant = view.find("*");
if (plant)
return {type: "eat", direction: plant};
if (space)
return {type: "move", direction: space};
};
}
class ExplodingBunnyRabbit extends PlantEater {
constructor () {
super()
}
act(view) {
super.act(view);
if(this.energy > 55) {
if (Math.random() < 0.25) {
return {type: "die"}
}
}
}
}
exports.LifelikeWorld=LifelikeWorld;
exports.BouncingCritter=life.BouncingCritter;
exports.Wall=life.Wall;
exports.PlantEater = PlantEater;
exports.Plant = Plant;
exports.actionTypes = actionTypes;

View File

@ -0,0 +1,136 @@
let life=require ("../life.js");
let plan = ["############################",
"# # # o ##",
"# #",
"# ##### #",
"## # # ## #",
"### ## # #",
"# ### # #",
"# #### #",
"# ## o #",
"# o # o ### #",
"# # #",
"############################"];
let Vector = life.Vector;
describe("Grid",
function() {
it("initially undefined",
function() {
let grid = new life.Grid(5, 5);
expect(grid.get(new life.Vector(1, 1))).toBe(undefined);
});
it("setting a value",
function() {
let grid = new life.Grid(5, 5);
grid.set(new Vector(1, 1), "X");
expect(grid.get(new Vector(1, 1))).toEqual("X");
});
it("forEach",
function() {
let grid = new life.Grid(5, 5);
let test = {grid: grid, sum: 0,
method: function () {
this.grid.forEach(function() { this.sum++; }, this);
}
};
test.grid.set(new Vector(2,3), "#");
test.grid.set(new Vector(3,4), "#");
test.method();
expect(test.sum).toBe(2);
});
});
describe("BouncingCritter",
function() {
let bob = null;
beforeEach(function () {
spyOn(Math, 'random').and.returnValue(0.5);
bob=new life.BouncingCritter();
});
it("constructor",
function() {
expect('direction' in bob).toBe(true);
expect(bob.direction).toBe('s');
});
it("act, clear path",
function () {
let clear = {look: function () {return " ";}};
expect(bob.act(clear)).toEqual({type: "move", direction: "s"});
});
it("act, unclear path",
function () {
let unclear = {look: function () {return "#";}, find: function () { return "n";}};
expect(bob.act(unclear)).toEqual({type: "move", direction: "n"});
});
});
describe("World",
function () {
it("roundtrip",
function() {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let rows = world.toString().split("\n");
// drop blank row
rows.pop();
expect(rows).toEqual(plan);
});
it("turn",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let count=0;
spyOn(world, 'letAct').and.callFake(function(critter,vector) {count++;});
world.turn();
expect(count).toBe(4);
});
it("checkDestination",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
expect(world.checkDestination({direction: 's'},
new life.Vector(19,1))).toEqual(new life.Vector(19,2));
expect(world.checkDestination({direction: 'n'},
new life.Vector(0,0))).toEqual(undefined);
});
it("letAct",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let src=new life.Vector(19,1);
let dest=new life.Vector(19,2);
let bob=world.grid.get(src);
spyOn(bob,'act').and.returnValue({type: 'move', direction: 's'});
world.letAct(bob, src);
expect(world.grid.get(dest)).toEqual(bob);
});
});
describe("View",
function () {
let world = new life.World(plan, {"#": life.Wall, "o": life.BouncingCritter});
let View=life.View;
let position=new Vector(15,9);
it("constructor",
function () {
let view=new View(world, position);
expect(view.vector).toEqual(position);
});
it("look",
function () {
let view=new View(world, position);
expect(view.look("s")).toBe(" ");
});
it("findAll",
function () {
let view=new View(world, position);
let directionNames = [ 'e', 'n', 'ne', 'nw', 's', 'se', 'sw', 'w' ];
expect(view.findAll(" ").sort()).toEqual(directionNames);
});
it("find",
function () {
let view=new View(world, position);
spyOn(Math, 'random').and.returnValue(0.5);
expect(view.find(" ")).toBe('s');
});
});

View File

@ -0,0 +1,103 @@
let life=require ("../moarlife.js");
let plan= ["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"];
describe("World",
function () {
let valley = new life.LifelikeWorld(plan,
{"#": life.Wall,
"O": life.PlantEater,
"*": life.Plant});
it("roundtrip",
function() {
let rows = valley.toString().split("\n");
// drop blank row
rows.pop();
expect(rows).toEqual(plan);
});
});
describe("actionTypes",
function () {
it("grow",
function () {
let critter = new life.Plant();
let energy = critter.energy;
life.actionTypes.grow(critter);
expect(critter.energy).toBeGreaterThan(energy);
});
})
describe("PlantEater",
function () {
it("constructor",
function () {
let pe = new life.PlantEater();
expect('energy' in pe).toBe(true);
expect(pe.energy).toBe(20);
});
it("act, reproduce",
function () {
let pe = new life.PlantEater();
pe.energy = 65
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "reproduce", direction: "n" });
});
it("act, eat",
function () {
let pe = new life.PlantEater();
pe.energy = 20
expect(pe.act({find: function (ch ) { if (ch === "*") return "n"; } })).toEqual({ type: "eat", direction: "n" });
});
it("act, move",
function () {
let pe = new life.PlantEater();
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "move", direction: "n" });
});
});
describe("ExplodingBunnyRabbit",
function () {
it("constructor",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect('energy' in pe).toBe(true);
expect(pe.energy).toBe(20);
});
it("act, reproduce",
function () {
let pe = new life.ExplodingBunnyRabbit();
pe.energy = 65
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "reproduce", direction: "n" });
});
it("act, eat",
function () {
let pe = new life.ExplodingBunnyRabbit();
pe.energy = 20
expect(pe.act({find: function (ch ) { if (ch === "*") return "n"; } })).toEqual({ type: "eat", direction: "n" });
});
it("act, move",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect(pe.act({find: function (ch) { if (ch === " ") return "n"; } })).toEqual({ type: "move", direction: "n" });
});
it("act, explode",
function () {
let pe = new life.ExplodingBunnyRabbit();
expect(pe.act({find: function (ch) { if (ch === "O") return "n"; } })).toEqual({ type: "explode", direction: "n" });
});
});

View File

@ -0,0 +1,13 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

28
assignments/A4/valley.js Normal file
View File

@ -0,0 +1,28 @@
let life=require("./moarlife.js");
let valley = new life.LifelikeWorld(
["############################",
"##### ######",
"## *** **##",
"# *##** ** O *##",
"# *** O ##** *#",
"# O ##*** #",
"# ##** #",
"# O #* #",
"#* #** O #",
"#*** ##** O **#",
"##**** ###*** *###",
"############################"],
{"#": life.Wall,
"O": life.PlantEater,
"*": life.Plant}
);
function loop () {
valley.turn();
console.log("\33c");
console.log(valley.toString());
setTimeout(function() { loop(); },250);
}
loop();

8
assignments/A5/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
assignments/A5/.idea/A5.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
assignments/A5/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (python-venv)" project-jdk-type="Python SDK" />
</project>

8
assignments/A5/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/A5.iml" filepath="$PROJECT_DIR$/.idea/A5.iml" />
</modules>
</component>
</project>

6
assignments/A5/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

1000
assignments/A5/2014-1000.csv Normal file

File diff suppressed because one or more lines are too long

122
assignments/A5/readcsv.py Normal file
View File

@ -0,0 +1,122 @@
def read_csv(filename):
"""Read a CSV file, return list of rows"""
import csv
with open(filename, 'rt', newline='') as f:
reader = csv.reader(f, skipinitialspace=True)
return [row for row in reader]
def header_map(headers):
"""
Read a list, and convert it to a dictionary where the key is each element of the given list and the value is
its position in the list
:param headers: List
:return: Dict
"""
header_dict = dict()
i = 0
for header in headers:
header_dict[header] = i
i = i + 1
return header_dict
def select(table, search_items):
"""
Read the set in the second argument and search through the table in the first argument and return the
columns that match the query in the search items
:param table: List
:param search_items: List
:return: List
"""
header_numbers = header_map(table[0])
ret_list = list()
columns = list()
for item in search_items:
if type(item) is int: # Convert searched elements into strings if it is a number
columns.append(header_numbers[str(item)])
else:
columns.append(header_numbers[item])
columns.sort()
for item in table:
lst = list()
for number in columns:
lst.append(item[number])
ret_list.append(lst)
return ret_list
def row2dict(hmap, row):
"""
Convert a row in the second argument, given the headers in the first argument to a dictionary which uses the
headers as keys and the row data as values
:param hmap: Dictionary
:param row: List
:return: Dictionary
"""
ret_dict = dict()
for key in hmap:
ret_dict[key] = row[hmap[key]]
return ret_dict
def check_row(row, query):
"""
Check the row in the first argument passes a query in the second argument. The second argument is a formatted
tuple where the first element in the tuple is a column name, the second is an operation (==, <=, >=, AND,
OR) and the third element is a condition, numeric or string matching. (ex: is age == 34, is color == blue). AND
and OR are special in that you can pass in recursive tuples (left and right argument are tuples) that will also
be evaluated recursively
:param row: List
:param query: Tuple
:return: Boolean
"""
def perform_operation(op, var, cond):
if type(var) is str:
if var.isnumeric():
var = int(var)
if type(cond) is str:
if cond.isnumeric():
cond = int(cond)
if op == '==':
return var == cond
elif op == '>=':
return var >= cond
elif op == '<=':
return var <= cond
elif op == 'OR':
return perform_operation(var[1], row[var[0]], var[2]) or perform_operation(cond[1], row[cond[0]], cond[2])
elif op == 'AND':
return perform_operation(var[1], row[var[0]], var[2]) and perform_operation(cond[1], row[cond[0]], cond[2])
else:
return False
if type(query[0]) and type(query[2]) is tuple:
return perform_operation(query[1], query[0], query[2])
else:
stringify = str(query[2])
return perform_operation(query[1], row[str(query[0])], stringify)
def filter_table(table, query):
"""
This function takes a table of csv values, and performs the query to filter out the rows that do not match the query
:param table: List
:param query: Tuple
:return: List
"""
header_row = header_map(table[0])
data_rows = table[1:]
result = list()
result.append(table[0])
for row in data_rows:
data_dict = row2dict(header_row, row)
if check_row(data_dict, query):
result.append(row)
return result

4
assignments/A5/test1.csv Normal file
View File

@ -0,0 +1,4 @@
name, age, eye colour
Bob, 5, blue
Mary, 27, brown
Vij, 54, green
1 name age eye colour
2 Bob 5 blue
3 Mary 27 brown
4 Vij 54 green

3
assignments/A5/test2.csv Normal file
View File

@ -0,0 +1,3 @@
name, 100
teddy, 500
lovely, 1000
1 name 100
2 teddy 500
3 lovely 1000

View File

@ -0,0 +1,114 @@
from readcsv import read_csv
from readcsv import header_map
from readcsv import select
from readcsv import row2dict
from readcsv import check_row
from readcsv import filter_table
table = read_csv('test1.csv')
sampledata = read_csv('2014-1000.csv')
def test_read_csv():
assert read_csv('test1.csv') == [['name', 'age', 'eye colour'],
['Bob', '5', 'blue'],
['Mary', '27', 'brown'],
['Vij', '54', 'green']]
def test_header_map_1():
hmap = header_map(table[0])
assert hmap == {'name': 0, 'age': 1, 'eye colour': 2}
def test_select_1():
assert select(table, {'name', 'eye colour'}) == [['name', 'eye colour'],
['Bob', 'blue'],
['Mary', 'brown'],
['Vij', 'green']]
def test_row2dict():
hmap = header_map(table[0])
assert row2dict(hmap, table[1]) == {'name': 'Bob', 'age': '5', 'eye colour': 'blue'}
def test_check_row():
row = {'name': 'Bob', 'age': '5', 'eye colour': 'blue'}
assert check_row(row, ('age', '==', 5))
assert not check_row(row, ('eye colour', '==', 5))
assert check_row(row, ('eye colour', '==', 'blue'))
assert check_row(row, ('age', '>=', 4))
assert check_row(row, ('age', '<=', 1000))
def test_check_row_logical():
row = {'name': 'Bob', 'age': '5', 'eye colour': 'blue'}
assert check_row(row, (('age', '==', 5), 'OR', ('eye colour', '==', 5)))
assert not check_row(row, (('age', '==', 5), 'AND', ('eye colour', '==', 5)))
def test_filter_table1():
assert filter_table(table, ('age', '>=', 0)) == [['name', 'age', 'eye colour'],
['Bob', '5', 'blue'],
['Mary', '27', 'brown'],
['Vij', '54', 'green']]
assert filter_table(table, ('age', '<=', 27)) == [['name', 'age', 'eye colour'],
['Bob', '5', 'blue'],
['Mary', '27', 'brown']]
assert filter_table(table, ('eye colour', '==', 'brown')) == [['name', 'age', 'eye colour'],
['Mary', '27', 'brown']]
assert filter_table(table, ('name', '==', 'Vij')) == [['name', 'age', 'eye colour'],
['Vij', '54', 'green']]
def test_filter_table2():
assert filter_table(table, (('age', '>=', 0), 'AND', ('age', '>=', '27'))) == [['name', 'age', 'eye colour'],
['Mary', '27', 'brown'],
['Vij', '54', 'green']]
assert filter_table(table, (('age', '<=', 27), 'AND', ('age', '>=', '27'))) == [['name', 'age', 'eye colour'],
['Mary', '27', 'brown']]
assert filter_table(table, (('eye colour', '==', 'brown'),
'OR',
('name', '==', 'Vij'))) == [['name', 'age', 'eye colour'],
['Mary', '27', 'brown'],
['Vij', '54', 'green']]
# Student Tests
table2 = read_csv('test2.csv')
hmap2 = header_map(table2[0])
def test_header_map2():
assert header_map(table2[0]) == {"name": 0, "100": 1}
def test_select2():
assert select(table2, [100]) == [["100"], ["500"], ["1000"]]
assert select(table2, ["name"]) == [["name"], ["teddy"], ["lovely"]]
assert select(table2, ["name", 100]) == [["name", "100"], ["teddy", "500"], ["lovely", "1000"]]
def test_row2dict2():
assert row2dict(hmap2, table2[1]) == {"name": "teddy", "100": "500"}
assert row2dict(hmap2, table2[2]) == {"name": "lovely", "100": "1000"}
def test_check_row_2():
row = {'name': 'Bob', 'age': '5', 'eye colour': 'blue'}
assert not check_row(row, ('age', '===', 5))
def test_check_row_3():
row = row2dict(hmap2, table2[1])
assert check_row(row, ("100", "==", 500))
def test_filter_table3():
assert filter_table(table2, ("100", ">=", 100)) == [["name", "100"], ["teddy", "500"], ["lovely", "1000"]]

File diff suppressed because it is too large Load Diff

32
assignments/A6/classify.m Normal file
View File

@ -0,0 +1,32 @@
iris = csvread("iris.csv");
[training, testing] = randomsplit(iris, 2/3)
p = 2
cells = p^(columns(iris)-1)+1
minmax = ranges(iris);
classes = minmax(2,1) - minmax(1,1) + 1;
votes = zeros(cells,classes);
for i=1:rows(training)
label = training(i,1);
hashval = hash(training(i,:), minmax, p);
votes(hashval,label) += 1;
endfor
classification = tally(votes)
correct = 0
for i=1:rows(testing);
hashval = hash(testing(i,:), minmax, p);
class=classification(hashval);
label = testing(i,1);
if label == class
correct += 1;
endif
endfor
display(correct/rows(testing))

151
assignments/A6/iris.csv Normal file
View File

@ -0,0 +1,151 @@
1,5.1,3.5,1.4,0.2
1,4.9,3.0,1.4,0.2
1,4.7,3.2,1.3,0.2
1,4.6,3.1,1.5,0.2
1,5.0,3.6,1.4,0.2
1,5.4,3.9,1.7,0.4
1,4.6,3.4,1.4,0.3
1,5.0,3.4,1.5,0.2
1,4.4,2.9,1.4,0.2
1,4.9,3.1,1.5,0.1
1,5.4,3.7,1.5,0.2
1,4.8,3.4,1.6,0.2
1,4.8,3.0,1.4,0.1
1,4.3,3.0,1.1,0.1
1,5.8,4.0,1.2,0.2
1,5.7,4.4,1.5,0.4
1,5.4,3.9,1.3,0.4
1,5.1,3.5,1.4,0.3
1,5.7,3.8,1.7,0.3
1,5.1,3.8,1.5,0.3
1,5.4,3.4,1.7,0.2
1,5.1,3.7,1.5,0.4
1,4.6,3.6,1.0,0.2
1,5.1,3.3,1.7,0.5
1,4.8,3.4,1.9,0.2
1,5.0,3.0,1.6,0.2
1,5.0,3.4,1.6,0.4
1,5.2,3.5,1.5,0.2
1,5.2,3.4,1.4,0.2
1,4.7,3.2,1.6,0.2
1,4.8,3.1,1.6,0.2
1,5.4,3.4,1.5,0.4
1,5.2,4.1,1.5,0.1
1,5.5,4.2,1.4,0.2
1,4.9,3.1,1.5,0.1
1,5.0,3.2,1.2,0.2
1,5.5,3.5,1.3,0.2
1,4.9,3.1,1.5,0.1
1,4.4,3.0,1.3,0.2
1,5.1,3.4,1.5,0.2
1,5.0,3.5,1.3,0.3
1,4.5,2.3,1.3,0.3
1,4.4,3.2,1.3,0.2
1,5.0,3.5,1.6,0.6
1,5.1,3.8,1.9,0.4
1,4.8,3.0,1.4,0.3
1,5.1,3.8,1.6,0.2
1,4.6,3.2,1.4,0.2
1,5.3,3.7,1.5,0.2
1,5.0,3.3,1.4,0.2
2,7.0,3.2,4.7,1.4
2,6.4,3.2,4.5,1.5
2,6.9,3.1,4.9,1.5
2,5.5,2.3,4.0,1.3
2,6.5,2.8,4.6,1.5
2,5.7,2.8,4.5,1.3
2,6.3,3.3,4.7,1.6
2,4.9,2.4,3.3,1.0
2,6.6,2.9,4.6,1.3
2,5.2,2.7,3.9,1.4
2,5.0,2.0,3.5,1.0
2,5.9,3.0,4.2,1.5
2,6.0,2.2,4.0,1.0
2,6.1,2.9,4.7,1.4
2,5.6,2.9,3.6,1.3
2,6.7,3.1,4.4,1.4
2,5.6,3.0,4.5,1.5
2,5.8,2.7,4.1,1.0
2,6.2,2.2,4.5,1.5
2,5.6,2.5,3.9,1.1
2,5.9,3.2,4.8,1.8
2,6.1,2.8,4.0,1.3
2,6.3,2.5,4.9,1.5
2,6.1,2.8,4.7,1.2
2,6.4,2.9,4.3,1.3
2,6.6,3.0,4.4,1.4
2,6.8,2.8,4.8,1.4
2,6.7,3.0,5.0,1.7
2,6.0,2.9,4.5,1.5
2,5.7,2.6,3.5,1.0
2,5.5,2.4,3.8,1.1
2,5.5,2.4,3.7,1.0
2,5.8,2.7,3.9,1.2
2,6.0,2.7,5.1,1.6
2,5.4,3.0,4.5,1.5
2,6.0,3.4,4.5,1.6
2,6.7,3.1,4.7,1.5
2,6.3,2.3,4.4,1.3
2,5.6,3.0,4.1,1.3
2,5.5,2.5,4.0,1.3
2,5.5,2.6,4.4,1.2
2,6.1,3.0,4.6,1.4
2,5.8,2.6,4.0,1.2
2,5.0,2.3,3.3,1.0
2,5.6,2.7,4.2,1.3
2,5.7,3.0,4.2,1.2
2,5.7,2.9,4.2,1.3
2,6.2,2.9,4.3,1.3
2,5.1,2.5,3.0,1.1
2,5.7,2.8,4.1,1.3
3,6.3,3.3,6.0,2.5
3,5.8,2.7,5.1,1.9
3,7.1,3.0,5.9,2.1
3,6.3,2.9,5.6,1.8
3,6.5,3.0,5.8,2.2
3,7.6,3.0,6.6,2.1
3,4.9,2.5,4.5,1.7
3,7.3,2.9,6.3,1.8
3,6.7,2.5,5.8,1.8
3,7.2,3.6,6.1,2.5
3,6.5,3.2,5.1,2.0
3,6.4,2.7,5.3,1.9
3,6.8,3.0,5.5,2.1
3,5.7,2.5,5.0,2.0
3,5.8,2.8,5.1,2.4
3,6.4,3.2,5.3,2.3
3,6.5,3.0,5.5,1.8
3,7.7,3.8,6.7,2.2
3,7.7,2.6,6.9,2.3
3,6.0,2.2,5.0,1.5
3,6.9,3.2,5.7,2.3
3,5.6,2.8,4.9,2.0
3,7.7,2.8,6.7,2.0
3,6.3,2.7,4.9,1.8
3,6.7,3.3,5.7,2.1
3,7.2,3.2,6.0,1.8
3,6.2,2.8,4.8,1.8
3,6.1,3.0,4.9,1.8
3,6.4,2.8,5.6,2.1
3,7.2,3.0,5.8,1.6
3,7.4,2.8,6.1,1.9
3,7.9,3.8,6.4,2.0
3,6.4,2.8,5.6,2.2
3,6.3,2.8,5.1,1.5
3,6.1,2.6,5.6,1.4
3,7.7,3.0,6.1,2.3
3,6.3,3.4,5.6,2.4
3,6.4,3.1,5.5,1.8
3,6.0,3.0,4.8,1.8
3,6.9,3.1,5.4,2.1
3,6.7,3.1,5.6,2.4
3,6.9,3.1,5.1,2.3
3,5.8,2.7,5.1,1.9
3,6.8,3.2,5.9,2.3
3,6.7,3.3,5.7,2.5
3,6.7,3.0,5.2,2.3
3,6.3,2.5,5.0,1.9
3,6.5,3.0,5.2,2.0
3,6.2,3.4,5.4,2.3
3,5.9,3.0,5.1,1.8
1 1 5.1 3.5 1.4 0.2
2 1 4.9 3.0 1.4 0.2
3 1 4.7 3.2 1.3 0.2
4 1 4.6 3.1 1.5 0.2
5 1 5.0 3.6 1.4 0.2
6 1 5.4 3.9 1.7 0.4
7 1 4.6 3.4 1.4 0.3
8 1 5.0 3.4 1.5 0.2
9 1 4.4 2.9 1.4 0.2
10 1 4.9 3.1 1.5 0.1
11 1 5.4 3.7 1.5 0.2
12 1 4.8 3.4 1.6 0.2
13 1 4.8 3.0 1.4 0.1
14 1 4.3 3.0 1.1 0.1
15 1 5.8 4.0 1.2 0.2
16 1 5.7 4.4 1.5 0.4
17 1 5.4 3.9 1.3 0.4
18 1 5.1 3.5 1.4 0.3
19 1 5.7 3.8 1.7 0.3
20 1 5.1 3.8 1.5 0.3
21 1 5.4 3.4 1.7 0.2
22 1 5.1 3.7 1.5 0.4
23 1 4.6 3.6 1.0 0.2
24 1 5.1 3.3 1.7 0.5
25 1 4.8 3.4 1.9 0.2
26 1 5.0 3.0 1.6 0.2
27 1 5.0 3.4 1.6 0.4
28 1 5.2 3.5 1.5 0.2
29 1 5.2 3.4 1.4 0.2
30 1 4.7 3.2 1.6 0.2
31 1 4.8 3.1 1.6 0.2
32 1 5.4 3.4 1.5 0.4
33 1 5.2 4.1 1.5 0.1
34 1 5.5 4.2 1.4 0.2
35 1 4.9 3.1 1.5 0.1
36 1 5.0 3.2 1.2 0.2
37 1 5.5 3.5 1.3 0.2
38 1 4.9 3.1 1.5 0.1
39 1 4.4 3.0 1.3 0.2
40 1 5.1 3.4 1.5 0.2
41 1 5.0 3.5 1.3 0.3
42 1 4.5 2.3 1.3 0.3
43 1 4.4 3.2 1.3 0.2
44 1 5.0 3.5 1.6 0.6
45 1 5.1 3.8 1.9 0.4
46 1 4.8 3.0 1.4 0.3
47 1 5.1 3.8 1.6 0.2
48 1 4.6 3.2 1.4 0.2
49 1 5.3 3.7 1.5 0.2
50 1 5.0 3.3 1.4 0.2
51 2 7.0 3.2 4.7 1.4
52 2 6.4 3.2 4.5 1.5
53 2 6.9 3.1 4.9 1.5
54 2 5.5 2.3 4.0 1.3
55 2 6.5 2.8 4.6 1.5
56 2 5.7 2.8 4.5 1.3
57 2 6.3 3.3 4.7 1.6
58 2 4.9 2.4 3.3 1.0
59 2 6.6 2.9 4.6 1.3
60 2 5.2 2.7 3.9 1.4
61 2 5.0 2.0 3.5 1.0
62 2 5.9 3.0 4.2 1.5
63 2 6.0 2.2 4.0 1.0
64 2 6.1 2.9 4.7 1.4
65 2 5.6 2.9 3.6 1.3
66 2 6.7 3.1 4.4 1.4
67 2 5.6 3.0 4.5 1.5
68 2 5.8 2.7 4.1 1.0
69 2 6.2 2.2 4.5 1.5
70 2 5.6 2.5 3.9 1.1
71 2 5.9 3.2 4.8 1.8
72 2 6.1 2.8 4.0 1.3
73 2 6.3 2.5 4.9 1.5
74 2 6.1 2.8 4.7 1.2
75 2 6.4 2.9 4.3 1.3
76 2 6.6 3.0 4.4 1.4
77 2 6.8 2.8 4.8 1.4
78 2 6.7 3.0 5.0 1.7
79 2 6.0 2.9 4.5 1.5
80 2 5.7 2.6 3.5 1.0
81 2 5.5 2.4 3.8 1.1
82 2 5.5 2.4 3.7 1.0
83 2 5.8 2.7 3.9 1.2
84 2 6.0 2.7 5.1 1.6
85 2 5.4 3.0 4.5 1.5
86 2 6.0 3.4 4.5 1.6
87 2 6.7 3.1 4.7 1.5
88 2 6.3 2.3 4.4 1.3
89 2 5.6 3.0 4.1 1.3
90 2 5.5 2.5 4.0 1.3
91 2 5.5 2.6 4.4 1.2
92 2 6.1 3.0 4.6 1.4
93 2 5.8 2.6 4.0 1.2
94 2 5.0 2.3 3.3 1.0
95 2 5.6 2.7 4.2 1.3
96 2 5.7 3.0 4.2 1.2
97 2 5.7 2.9 4.2 1.3
98 2 6.2 2.9 4.3 1.3
99 2 5.1 2.5 3.0 1.1
100 2 5.7 2.8 4.1 1.3
101 3 6.3 3.3 6.0 2.5
102 3 5.8 2.7 5.1 1.9
103 3 7.1 3.0 5.9 2.1
104 3 6.3 2.9 5.6 1.8
105 3 6.5 3.0 5.8 2.2
106 3 7.6 3.0 6.6 2.1
107 3 4.9 2.5 4.5 1.7
108 3 7.3 2.9 6.3 1.8
109 3 6.7 2.5 5.8 1.8
110 3 7.2 3.6 6.1 2.5
111 3 6.5 3.2 5.1 2.0
112 3 6.4 2.7 5.3 1.9
113 3 6.8 3.0 5.5 2.1
114 3 5.7 2.5 5.0 2.0
115 3 5.8 2.8 5.1 2.4
116 3 6.4 3.2 5.3 2.3
117 3 6.5 3.0 5.5 1.8
118 3 7.7 3.8 6.7 2.2
119 3 7.7 2.6 6.9 2.3
120 3 6.0 2.2 5.0 1.5
121 3 6.9 3.2 5.7 2.3
122 3 5.6 2.8 4.9 2.0
123 3 7.7 2.8 6.7 2.0
124 3 6.3 2.7 4.9 1.8
125 3 6.7 3.3 5.7 2.1
126 3 7.2 3.2 6.0 1.8
127 3 6.2 2.8 4.8 1.8
128 3 6.1 3.0 4.9 1.8
129 3 6.4 2.8 5.6 2.1
130 3 7.2 3.0 5.8 1.6
131 3 7.4 2.8 6.1 1.9
132 3 7.9 3.8 6.4 2.0
133 3 6.4 2.8 5.6 2.2
134 3 6.3 2.8 5.1 1.5
135 3 6.1 2.6 5.6 1.4
136 3 7.7 3.0 6.1 2.3
137 3 6.3 3.4 5.6 2.4
138 3 6.4 3.1 5.5 1.8
139 3 6.0 3.0 4.8 1.8
140 3 6.9 3.1 5.4 2.1
141 3 6.7 3.1 5.6 2.4
142 3 6.9 3.1 5.1 2.3
143 3 5.8 2.7 5.1 1.9
144 3 6.8 3.2 5.9 2.3
145 3 6.7 3.3 5.7 2.5
146 3 6.7 3.0 5.2 2.3
147 3 6.3 2.5 5.0 1.9
148 3 6.5 3.0 5.2 2.0
149 3 6.2 3.4 5.4 2.3
150 3 5.9 3.0 5.1 1.8

View File

@ -0,0 +1 @@
# Created by Octave 7.3.0, Wed Dec 07 23:43:44 2022 GMT <unknown@Isaac-DesktopPC>

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="client" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="L14" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/client.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="fizzbuzz" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="L14" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/fizzbuzz.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="humansize" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="L14" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/humansize.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="pytest" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="pytest --cov --cov-report=term-missing" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

8
labs/L15/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
labs/L15/.idea/L15.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10 (python-venv)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
labs/L15/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (python-venv)" project-jdk-type="Python SDK" />
</project>

8
labs/L15/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/L15.iml" filepath="$PROJECT_DIR$/.idea/L15.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="globex" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="L15" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="C:\Users\Isaac\Documents\CS2613-Repo\cs2613-ishoebot\labs\L15\globex.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="pytest" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="pytest --cov --cov-report=term-missing" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

6
labs/L15/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

22
labs/L15/globex.py Normal file
View File

@ -0,0 +1,22 @@
import glob
import os
# new_dir = os.path.expanduser("~/fcshome/cs2613/labs/test") # For lab machines
new_dir = os.path.abspath("C:\\Users\\Isaac\\Documents\\CS2613-Repo\\cs2613-ishoebot\\labs\\L14") # For local machine
python_files_for = []
for file in glob.glob("*.py"):
python_files_for.append(os.path.join(new_dir, file))
python_files_comp = [os.path.join(new_dir, file) for file in glob.glob("*.py")]
python_files_map = map(lambda file: os.path.join(new_dir, file), glob.glob("*.py"))
if __name__ == '__main__': # pragma: no cover
print(python_files_for)
print()
print(python_files_comp)
print()
print(list(python_files_map))

7
labs/L15/list2dict.py Normal file
View File

@ -0,0 +1,7 @@
def list2dict(lst):
lst_dict = dict()
counter = 1
for i in range(lst):
lst_dict[counter] = i
counter += 1
return lst_dict

9
labs/L15/test_globex.py Normal file
View File

@ -0,0 +1,9 @@
import globex
def test_for():
assert sorted(globex.python_files_for) == sorted(globex.python_files_comp)
def test_map():
assert sorted(globex.python_files_comp) == sorted(globex.python_files_map)

View File

@ -0,0 +1,10 @@
from list2dict import list2dict
def test_empty():
assert list2dict([]) == {}
def test_abc():
dictionary = list2dict(["a", "b", "c"])
assert dictionary == {1: 'a', 2: 'b', 3: 'c'}

8
labs/L16/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
labs/L16/.idea/L16.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
labs/L16/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="CS2613-venv" project-jdk-type="Python SDK" />
</project>

8
labs/L16/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/L16.iml" filepath="$PROJECT_DIR$/.idea/L16.iml" />
</modules>
</component>
</project>

6
labs/L16/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

14
labs/L16/parse_csv.py Normal file
View File

@ -0,0 +1,14 @@
import re
def split_csv(string):
return [row.split(",") for row in string.splitlines()]
def strip_quotes(string):
strip_regex = re.compile(r'("?)*$("?)')
search = strip_regex.search(string)
if search:
return search.group(1)
else:
return None

View File

@ -0,0 +1,45 @@
from parse_csv import split_csv
from parse_csv import strip_quotes
test_string_1 = """OPEID,INSTNM,TUITIONFEE_OUT
02503400,Amridge University,6900
00100700,Central Alabama Community College,7770
01218200,Chattahoochee Valley Community College,7830
00101500,Enterprise State Community College,7770
00106000,James H Faulkner State Community College,7770
00101700,Gadsden State Community College,5976
00101800,George C Wallace State Community College-Dothan,7710
"""
table1 = [['OPEID', 'INSTNM', 'TUITIONFEE_OUT'],
['02503400', 'Amridge University', '6900'],
['00100700', 'Central Alabama Community College', '7770'],
['01218200', 'Chattahoochee Valley Community College', '7830'],
['00101500', 'Enterprise State Community College', '7770'],
['00106000', 'James H Faulkner State Community College', '7770'],
['00101700', 'Gadsden State Community College', '5976'],
['00101800', 'George C Wallace State Community College-Dothan', '7710']]
def test_split_1():
assert split_csv(test_string_1) == table1
test_string_2 = '''OPEID,INSTNM,TUITIONFEE_OUT
02503400,"Amridge University",6900
00100700,"Central Alabama Community College",7770
01218200,"Chattahoochee Valley Community College",7830
00101500,"Enterprise State Community College",7770
00106000,"James H Faulkner State Community College",7770
00101700,"Gadsden State Community College",5976
00101800,"George C Wallace State Community College-Dothan",7710
'''
def test_split_2():
assert split_csv(test_string_2) == table1
def test_strip_quotes():
assert strip_quotes('"hello"') == 'hello'
assert strip_quotes('hello') == 'hello'

8
labs/L17/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
labs/L17/.idea/L17.iml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="CS2613-venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
labs/L17/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="CS2613-venv" project-jdk-type="Python SDK" />
</project>

8
labs/L17/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/L17.iml" filepath="$PROJECT_DIR$/.idea/L17.iml" />
</modules>
</component>
</project>

6
labs/L17/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

16
labs/L17/main.py Normal file
View File

@ -0,0 +1,16 @@
# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
def print_hi(name):
# Use a breakpoint in the code line below to debug your script.
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
print_hi('PyCharm')
# See PyCharm help at https://www.jetbrains.com/help/pycharm/

View File

@ -0,0 +1,22 @@
#lang racket
(define (balance lst)
(define (helper lst counter)
(cond
[(empty? lst) counter] ;;base case
[(list? (first lst)) (helper (rest lst) counter)] ;;unwrap list
[(eq? (first lst) 'debit) (helper (rest lst) (- counter last))] ;;if debit subtract the amount
[(eq? (first lst) 'credit) (helper (rest lst) (+ counter last))] ;;if credit add the amount
[else (helper (rest lst) counter)]
)
)
(helper lst 0)
)
;; Racket
(module+ test
(require rackunit)
(check-equal? (balance (list (list 'credit 5))) 5)
(check-equal? (balance (list '(debit 5))) -5)
(check-equal? (balance '((debit 11) (credit 3))) -8)
(check-equal? (balance '((debit 3) (credit 5))) 2)
(check-equal? (balance '((debit 5) (credit 23) (debit 23) (credit 5))) 0))

View File

@ -0,0 +1,19 @@
def invert(lst):
lst = list(lst) # Makes sure arugment is a list
#list_range = range(0, len(lst)) # Code I might have used if I used a dictionary comprehension
ret_dict = dict() # Create empty dict
counter = 0 # Create counter
for i in lst:
ret_dict[i] = counter # Assign each element of list to
# its postion in list
counter = counter + 1 # Increment counter
if (len(lst) > len(ret_dict)): # Check if the length of new dict is less than
return None # input list, if so, there is duplicates so return none
return ret_dict # Return created dictionary

View File

@ -0,0 +1,13 @@
from invert import invert
def test_empty():
assert invert([]) == {}
def test_simple():
invert(["three","two","one"]) == {"three": 0, "two":1, "one":2}
def test_duplicate():
assert invert(["bob","bob"]) == None
def test_numeric():
assert invert(range(0,6)) == { 0:0, 1:1, 2:2, 3:3, 4:4, 5:5 }
def test_invert():
L=[-8,"pineapple",3]
D=invert(L)
assert [ L[D[j]] for j in L ] == L

View File

@ -0,0 +1,50 @@
class Expression {
constructor(op, left, right) {
this.op = op
this.left = left
this.right = right
}
eval() {
return evalExpression(this)
}
}
function evalExpression(expr) {
let tempLeft
let tempRight
if (typeof(expr.left) === "object") { // Check if left type is another expression
tempLeft = evalExpression(expr.left)
}
else {
tempLeft = expr.left
}
if (typeof(expr.right) === "object") { // Check if right type is another expression
tempRight = evalExpression(expr.right)
}
else {
tempRight = expr.right
}
if (typeof(tempLeft) === "number" & typeof(tempRight) === "number") { // Make sure both inputs are number
if (expr.op === "+") {
return tempLeft + tempRight
}
else if(expr.op === "-") {
return tempLeft - tempRight
}
else if(expr.op === "*") {
return tempLeft * tempRight
}
else if(expr.op === "/") {
return tempLeft / tempRight
}
else { // Case for when there is no valid provided operator
return "Invalid operator syntax"
}
}
else { //Case for when the left or right are not numbers
return "Invalid number syntax"
}
}
exports.Expression = Expression;

View File

@ -0,0 +1,71 @@
Expression = require("../expression.js").Expression;
let six_plus_nine = new Expression('+', 6, 9);
let six_times_nine = new Expression('*', 6, 9);
let six_minus_nine = new Expression('-', 6, 9);
let sixteen_div_eight = new Expression('/', 16, 8);
let compound1 = new Expression('+', six_times_nine, six_plus_nine)
let compound2 = new Expression('*', six_times_nine, compound1)
let compound3 = new Expression('+', compound2, 3)
describe("constructor",
function() {
let one = new Expression("+",0,1);
it("not null", () => expect(one).not.toBe(null));
it("op", () => expect(one.op).toBe("+"));
it("left", () => expect(one.left).toBe(0));
it("right", () => expect(one.right).toBe(1));
});
describe("simple",
function() {
it("+", () => expect(six_plus_nine.eval()).toBe(15));
it("-", () => expect(six_minus_nine.eval()).toBe(-3));
it("*", () => expect(six_times_nine.eval()).toBe(54));
it("/", () => expect(sixteen_div_eight.eval()).toBe(2));
});
describe("compound",
function() {
it("1", () => expect(compound1.eval()).toBe(69));
it("2", () => expect(compound2.eval()).toBe(3726));
it("3", () => expect(compound3.eval()).toBe(3729));
});
describe("floating point", //Testing floating point
function() {
let addPointFive = new Expression("+", 0.5, 0.5)
let timesPointFive = new Expression("*", 0.5, 10)
let minusPointFive = new Expression("-", 10, 0.5)
let dividePointFive = new Expression("/", 0.5, 0.1)
it("+", () => expect(addPointFive.eval()).toBe(1.0));
it("*", () => expect(timesPointFive.eval()).toBe(5.0));
it("-", () => expect(minusPointFive.eval()).toBe(9.5));
it("/", () => expect(dividePointFive.eval()).toBe(5.0));
});
describe("bad input", //Testing bad inputs
function() {
let invalidStringLeft = new Expression("+", "five", 6)
let invalidStringRight = new Expression("+", 5, "six")
let invalidOperator = new Expression("^", 6, 9)
it("Invalid String on left side", () => expect(invalidStringLeft.eval()).toBe("Invalid number syntax"));
it("Invalid String on right side", () => expect(invalidStringRight.eval()).toBe("Invalid number syntax"));
it("invalid operator", () => expect(invalidOperator.eval()).toBe("Invalid operator syntax"));
});
describe("compound bad input", //Testing bad inputs in compound cases
function() {
let invalidNumber = new Expression("+", "five", 6)
let invalidOperator = new Expression("^", 6, 9)
let semiValidCompound = new Expression("+", six_plus_nine, invalidNumber)
let completlyBadCompound = new Expression("+", invalidNumber, invalidOperator)
it("semi-valid", () => expect(semiValidCompound.eval()).toBe("Invalid number syntax"));
it("invalid", () => expect(completlyBadCompound.eval()).toBe("Invalid number syntax")); //Expected to be invalid number because it is the first error to be encountered
});

View File

@ -0,0 +1,13 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

71
tests/Final/tests.txt Normal file
View File

@ -0,0 +1,71 @@
;; Racket
(module+ test
(require rackunit)
(check-equal? (balance (list (list 'credit 5))) 5)
(check-equal? (balance (list '(debit 5))) -5)
(check-equal? (balance '((debit 11) (credit 3))) -8)
(check-equal? (balance '((debit 3) (credit 5))) 2)
(check-equal? (balance '((debit 5) (credit 23) (debit 23) (credit 5))) 0))
# Python
from invert import invert
def test_empty():
assert invert([]) == {}
def test_simple():
invert(["three","two","one"]) == {"three": 0, "two":1, "one":2}
def test_duplicate():
assert invert(["bob","bob"]) == None
def test_numeric():
assert invert(range(0,6)) == { 0:0, 1:1, 2:2, 3:3, 4:4, 5:5 }
def test_invert():
L=[-8,"pineapple",3]
D=invert(L)
assert [ L[D[j]] for j in L ] == L
// JavaScript
Expression = require("../expression.js").Expression;
let six_plus_nine = new Expression('+', 6, 9);
let six_times_nine = new Expression('*', 6, 9);
let six_minus_nine = new Expression('-', 6, 9);
let sixteen_div_eight = new Expression('/', 16, 8);
let compound1 = new Expression('+', six_times_nine, six_plus_nine)
let compound2 = new Expression('*', six_times_nine, compound1)
let compound3 = new Expression('+', compound2, 3)
describe("constructor",
function() {
let one = new Expression("+",0,1);
it("not null", () => expect(one).not.toBe(null));
it("op", () => expect(one.op).toBe("+"));
it("left", () => expect(one.left).toBe(0));
it("right", () => expect(one.right).toBe(1));
});
describe("simple",
function() {
it("+", () => expect(six_plus_nine.eval()).toBe(15));
it("-", () => expect(six_minus_nine.eval()).toBe(-3));
it("*", () => expect(six_times_nine.eval()).toBe(54));
it("/", () => expect(sixteen_div_eight.eval()).toBe(2));
});
describe("compound",
function() {
it("1", () => expect(compound1.eval()).toBe(69));
it("2", () => expect(compound2.eval()).toBe(3726));
it("3", () => expect(compound3.eval()).toBe(3729));
});
# Octave
%!shared P1,P3
%! P1=[7,5,-3];
%! P3=[1,2,3;4,5,6;7,8,9];
%
%!assert(manypoly(P1,0),7,eps)
%!assert(manypoly(P1,1),9,eps)
%!assert(manypoly(P1,5),7+5*5-3*25,eps)
%!assert(manypoly(P3,0),[1;4;7],eps)
%!assert(manypoly(P3,1),[6;15;24],eps)
%!assert(manypoly(P3,2),[1+2*2+3*4;4+5*2+6*4;7+8*2+9*4],eps)

4
tests/Q3/prefix.py Normal file
View File

@ -0,0 +1,4 @@
def with_prefix(prefixes, words):
for prefix in prefixes:
lst = [word for word in words if word.startswith(prefix)]
yield lst

41
tests/Q3/test_prefix.py Normal file
View File

@ -0,0 +1,41 @@
from prefix import with_prefix
words=["apple","baby","abba"]
a_words=["apple", "abba"]
def test_simple():
assert list(with_prefix(["a"],words)) == [a_words]
def test_order():
assert list(with_prefix(["b","a"],words)) == [["baby"], a_words]
def test_multi():
assert list(with_prefix(["bb","ab"],words)) == [[],["abba"]]
# Commented out because the solution I am submitting is not using regex
#def test_regex1():
# assert list(with_prefix(["[a-z]b"], words)) == [ ["abba"] ]
#def test_regex2():
# assert list(with_prefix([".*a$"], words)) == [ ["abba"] ]
def test_gen():
gen = with_prefix(["b"],words)
assert next(gen) == ["baby"]
def test_gen2(): #Testing out of order prefixes, with generator syntax
gen = with_prefix(["b", "a"], words)
assert next(gen) == ["baby"]
assert next(gen) == ["apple", "abba"]
def test_gen3(): #Testing out returning the same number of elements as words, out of order
gen = with_prefix(["bab", "abb", "app"], words)
assert next(gen) == ["baby"]
assert next(gen) == ["abba"]
assert next(gen) == ["apple"]
words2 = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "z"]
def test_gen4(): #Testing a long word and one letter word
gen = with_prefix(["a", "z"], words2)
assert next(gen) == ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
assert next(gen) == ["z"]

View File

@ -0,0 +1,32 @@
def is_word(word):
counter = 0
for l in word:
if (counter % 2) == 0: #zero is vowel, one is constanant
if l == 'a' or l == 'e' or l == 'i' or l == 'o' or l == 'u':
counter = counter + 1
else:
return False
else:
if l == 'b' or l == 'k' or l == 'p' or l == 't' or l == 'z':
counter = counter + 1
else:
return False
return True
def cycle(lst):
while True:
yield lst
x = lst[0]
lst = lst[1:]
lst.append(x)
class Skippy:
def __init__(self, lst, offset)
self.lst = lst
self.offset = offset
self.counter = 0
def __next__(self)
if self.counter > length(self.lst)
self.counter = 0

View File

@ -0,0 +1,37 @@
from practice_questions import is_word
def test_match():
assert is_word("akataka") == True
assert is_word("ububu") == True
assert is_word("ikekezaza") == True
def test_extra():
assert is_word("akatakaa") == False
assert is_word("uububu") == False
def test_bad_letter():
assert is_word("yakataka") == False
assert is_word("akatakala") == False
def test_consonant_start():
assert is_word("kakataka") == False
assert is_word("bububu") == False
from practice_questions import cycle
def test_small():
lst = [1,2,3]
g = cycle(lst)
assert next(g) == lst
assert next(g) == [2,3,1]
assert next(g) == [3,1,2]
def test_big():
n = 5000
lst = list(range(n))
g = cycle(lst)
for j in range(n):
lst2 = next(g)
assert lst2[0] == n-1
lst3 = next(g)
assert lst3==lst

View File

@ -0,0 +1 @@
pytest --cov --cov-report=term-missing

Binary file not shown.

View File

@ -1,4 +0,0 @@
# created by virtualenv automatically
# Commit venv because it is shared between all of python environments for this class
# *

View File

@ -1,166 +0,0 @@
import sys
import os
import re
import importlib
import warnings
import contextlib
is_pypy = '__pypy__' in sys.builtin_module_names
warnings.filterwarnings('ignore',
r'.+ distutils\b.+ deprecated',
DeprecationWarning)
def warn_distutils_present():
if 'distutils' not in sys.modules:
return
if is_pypy and sys.version_info < (3, 7):
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
return
warnings.warn(
"Distutils was imported before Setuptools, but importing Setuptools "
"also replaces the `distutils` module in `sys.modules`. This may lead "
"to undesirable behaviors or errors. To avoid these issues, avoid "
"using distutils directly, ensure that setuptools is installed in the "
"traditional way (e.g. not an editable install), and/or make sure "
"that setuptools is always imported before distutils.")
def clear_distutils():
if 'distutils' not in sys.modules:
return
warnings.warn("Setuptools is replacing distutils.")
mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
for name in mods:
del sys.modules[name]
def enabled():
"""
Allow selection of distutils by environment variable.
"""
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
return which == 'local'
def ensure_local_distutils():
clear_distutils()
# With the DistutilsMetaFinder in place,
# perform an import to cause distutils to be
# loaded from setuptools._distutils. Ref #2906.
with shim():
importlib.import_module('distutils')
# check that submodules load as expected
core = importlib.import_module('distutils.core')
assert '_distutils' in core.__file__, core.__file__
def do_override():
"""
Ensure that the local copy of distutils is preferred over stdlib.
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
for more motivation.
"""
if enabled():
warn_distutils_present()
ensure_local_distutils()
class DistutilsMetaFinder:
def find_spec(self, fullname, path, target=None):
if path is not None:
return
method_name = 'spec_for_{fullname}'.format(**locals())
method = getattr(self, method_name, lambda: None)
return method()
def spec_for_distutils(self):
import importlib.abc
import importlib.util
try:
mod = importlib.import_module('setuptools._distutils')
except Exception:
# There are a couple of cases where setuptools._distutils
# may not be present:
# - An older Setuptools without a local distutils is
# taking precedence. Ref #2957.
# - Path manipulation during sitecustomize removes
# setuptools from the path but only after the hook
# has been loaded. Ref #2980.
# In either case, fall back to stdlib behavior.
return
class DistutilsLoader(importlib.abc.Loader):
def create_module(self, spec):
return mod
def exec_module(self, module):
pass
return importlib.util.spec_from_loader('distutils', DistutilsLoader())
def spec_for_pip(self):
"""
Ensure stdlib distutils when running under pip.
See pypa/pip#8761 for rationale.
"""
if self.pip_imported_during_build():
return
clear_distutils()
self.spec_for_distutils = lambda: None
@classmethod
def pip_imported_during_build(cls):
"""
Detect if pip is being imported in a build script. Ref #2355.
"""
import traceback
return any(
cls.frame_file_is_setup(frame)
for frame, line in traceback.walk_stack(None)
)
@staticmethod
def frame_file_is_setup(frame):
"""
Return True if the indicated frame suggests a setup.py file.
"""
# some frames may not have __file__ (#2940)
return frame.f_globals.get('__file__', '').endswith('setup.py')
DISTUTILS_FINDER = DistutilsMetaFinder()
def add_shim():
DISTUTILS_FINDER in sys.meta_path or insert_shim()
@contextlib.contextmanager
def shim():
insert_shim()
try:
yield
finally:
remove_shim()
def insert_shim():
sys.meta_path.insert(0, DISTUTILS_FINDER)
def remove_shim():
try:
sys.meta_path.remove(DISTUTILS_FINDER)
except ValueError:
pass

View File

@ -1 +0,0 @@
__import__('_distutils_hack').do_override()

View File

@ -1,9 +0,0 @@
__all__ = ["__version__", "version_tuple"]
try:
from ._version import version as __version__, version_tuple
except ImportError: # pragma: no cover
# broken installation, we don't even try
# unknown only works because we do poor mans version compare
__version__ = "unknown"
version_tuple = (0, 0, "unknown") # type:ignore[assignment]

View File

@ -1,116 +0,0 @@
"""Allow bash-completion for argparse with argcomplete if installed.
Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
to find the magic string, so _ARGCOMPLETE env. var is never set, and
this does not need special code).
Function try_argcomplete(parser) should be called directly before
the call to ArgumentParser.parse_args().
The filescompleter is what you normally would use on the positional
arguments specification, in order to get "dirname/" after "dirn<TAB>"
instead of the default "dirname ":
optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
Other, application specific, completers should go in the file
doing the add_argument calls as they need to be specified as .completer
attributes as well. (If argcomplete is not installed, the function the
attribute points to will not be used).
SPEEDUP
=======
The generic argcomplete script for bash-completion
(/etc/bash_completion.d/python-argcomplete.sh)
uses a python program to determine startup script generated by pip.
You can speed up completion somewhat by changing this script to include
# PYTHON_ARGCOMPLETE_OK
so the python-argcomplete-check-easy-install-script does not
need to be called to find the entry point of the code and see if that is
marked with PYTHON_ARGCOMPLETE_OK.
INSTALL/DEBUGGING
=================
To include this support in another application that has setup.py generated
scripts:
- Add the line:
# PYTHON_ARGCOMPLETE_OK
near the top of the main python entry point.
- Include in the file calling parse_args():
from _argcomplete import try_argcomplete, filescompleter
Call try_argcomplete just before parse_args(), and optionally add
filescompleter to the positional arguments' add_argument().
If things do not work right away:
- Switch on argcomplete debugging with (also helpful when doing custom
completers):
export _ARC_DEBUG=1
- Run:
python-argcomplete-check-easy-install-script $(which appname)
echo $?
will echo 0 if the magic line has been found, 1 if not.
- Sometimes it helps to find early on errors using:
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script).
"""
import argparse
import os
import sys
from glob import glob
from typing import Any
from typing import List
from typing import Optional
class FastFilesCompleter:
"""Fast file completer class."""
def __init__(self, directories: bool = True) -> None:
self.directories = directories
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
# Only called on non option completions.
if os.path.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
else:
prefix_dir = 0
completion = []
globbed = []
if "*" not in prefix and "?" not in prefix:
# We are on unix, otherwise no bash.
if not prefix or prefix[-1] == os.path.sep:
globbed.extend(glob(prefix + ".*"))
prefix += "*"
globbed.extend(glob(prefix))
for x in sorted(globbed):
if os.path.isdir(x):
x += "/"
# Append stripping the prefix (like bash, not like compgen).
completion.append(x[prefix_dir:])
return completion
if os.environ.get("_ARGCOMPLETE"):
try:
import argcomplete.completers
except ImportError:
sys.exit(-1)
filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
argcomplete.autocomplete(parser, always_complete_options=False)
else:
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
pass
filescompleter = None

View File

@ -1,22 +0,0 @@
"""Python inspection/code generation API."""
from .code import Code
from .code import ExceptionInfo
from .code import filter_traceback
from .code import Frame
from .code import getfslineno
from .code import Traceback
from .code import TracebackEntry
from .source import getrawcode
from .source import Source
__all__ = [
"Code",
"ExceptionInfo",
"filter_traceback",
"Frame",
"getfslineno",
"getrawcode",
"Traceback",
"TracebackEntry",
"Source",
]

File diff suppressed because it is too large Load Diff

View File

@ -1,217 +0,0 @@
import ast
import inspect
import textwrap
import tokenize
import types
import warnings
from bisect import bisect_right
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Tuple
from typing import Union
class Source:
"""An immutable object holding a source code fragment.
When using Source(...), the source lines are deindented.
"""
def __init__(self, obj: object = None) -> None:
if not obj:
self.lines: List[str] = []
elif isinstance(obj, Source):
self.lines = obj.lines
elif isinstance(obj, (tuple, list)):
self.lines = deindent(x.rstrip("\n") for x in obj)
elif isinstance(obj, str):
self.lines = deindent(obj.split("\n"))
else:
try:
rawcode = getrawcode(obj)
src = inspect.getsource(rawcode)
except TypeError:
src = inspect.getsource(obj) # type: ignore[arg-type]
self.lines = deindent(src.split("\n"))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Source):
return NotImplemented
return self.lines == other.lines
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
@overload
def __getitem__(self, key: int) -> str:
...
@overload
def __getitem__(self, key: slice) -> "Source":
...
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
if isinstance(key, int):
return self.lines[key]
else:
if key.step not in (None, 1):
raise IndexError("cannot slice a Source with a step")
newsource = Source()
newsource.lines = self.lines[key.start : key.stop]
return newsource
def __iter__(self) -> Iterator[str]:
return iter(self.lines)
def __len__(self) -> int:
return len(self.lines)
def strip(self) -> "Source":
"""Return new Source object with trailing and leading blank lines removed."""
start, end = 0, len(self)
while start < end and not self.lines[start].strip():
start += 1
while end > start and not self.lines[end - 1].strip():
end -= 1
source = Source()
source.lines[:] = self.lines[start:end]
return source
def indent(self, indent: str = " " * 4) -> "Source":
"""Return a copy of the source object with all lines indented by the
given indent-string."""
newsource = Source()
newsource.lines = [(indent + line) for line in self.lines]
return newsource
def getstatement(self, lineno: int) -> "Source":
"""Return Source statement which contains the given linenumber
(counted from 0)."""
start, end = self.getstatementrange(lineno)
return self[start:end]
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
"""Return (start, end) tuple which spans the minimal statement region
which containing the given lineno."""
if not (0 <= lineno < len(self)):
raise IndexError("lineno out of range")
ast, start, end = getstatementrange_ast(lineno, self)
return start, end
def deindent(self) -> "Source":
"""Return a new Source object deindented."""
newsource = Source()
newsource.lines[:] = deindent(self.lines)
return newsource
def __str__(self) -> str:
return "\n".join(self.lines)
#
# helper functions
#
def findsource(obj) -> Tuple[Optional[Source], int]:
try:
sourcelines, lineno = inspect.findsource(obj)
except Exception:
return None, -1
source = Source()
source.lines = [line.rstrip() for line in sourcelines]
return source, lineno
def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
"""Return code object for given function."""
try:
return obj.__code__ # type: ignore[attr-defined,no-any-return]
except AttributeError:
pass
if trycall:
call = getattr(obj, "__call__", None)
if call and not isinstance(obj, type):
return getrawcode(call, trycall=False)
raise TypeError(f"could not get code object for {obj!r}")
def deindent(lines: Iterable[str]) -> List[str]:
return textwrap.dedent("\n".join(lines)).splitlines()
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
# Flatten all statements and except handlers into one lineno-list.
# AST's line numbers start indexing at 1.
values: List[int] = []
for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator.
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
for d in x.decorator_list:
values.append(d.lineno - 1)
values.append(x.lineno - 1)
for name in ("finalbody", "orelse"):
val: Optional[List[ast.stmt]] = getattr(x, name, None)
if val:
# Treat the finally/orelse part as its own statement.
values.append(val[0].lineno - 1 - 1)
values.sort()
insert_index = bisect_right(values, lineno)
start = values[insert_index - 1]
if insert_index >= len(values):
end = None
else:
end = values[insert_index]
return start, end
def getstatementrange_ast(
lineno: int,
source: Source,
assertion: bool = False,
astnode: Optional[ast.AST] = None,
) -> Tuple[ast.AST, int, int]:
if astnode is None:
content = str(source)
# See #4260:
# Don't produce duplicate warnings when compiling source to find AST.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
astnode = ast.parse(content, "source", "exec")
start, end = get_statement_startend2(lineno, astnode)
# We need to correct the end:
# - ast-parsing strips comments
# - there might be empty lines
# - we might have lesser indented code blocks at the end
if end is None:
end = len(source.lines)
if end > start + 1:
# Make sure we don't span differently indented code blocks
# by using the BlockFinder helper used which inspect.getsource() uses itself.
block_finder = inspect.BlockFinder()
# If we start with an indented line, put blockfinder to "started" mode.
block_finder.started = source.lines[start][0].isspace()
it = ((x + "\n") for x in source.lines[start:end])
try:
for tok in tokenize.generate_tokens(lambda: next(it)):
block_finder.tokeneater(*tok)
except (inspect.EndOfBlock, IndentationError):
end = block_finder.last + start
except Exception:
pass
# The end might still point to a comment or empty line, correct it.
while end:
line = source.lines[end - 1].lstrip()
if line.startswith("#") or not line:
end -= 1
else:
break
return astnode, start, end

View File

@ -1,8 +0,0 @@
from .terminalwriter import get_terminal_width
from .terminalwriter import TerminalWriter
__all__ = [
"TerminalWriter",
"get_terminal_width",
]

View File

@ -1,180 +0,0 @@
import pprint
import reprlib
from typing import Any
from typing import Dict
from typing import IO
from typing import Optional
def _try_repr_or_str(obj: object) -> str:
try:
return repr(obj)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException:
return f'{type(obj).__name__}("{obj}")'
def _format_repr_exception(exc: BaseException, obj: object) -> str:
try:
exc_info = _try_repr_or_str(exc)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
exc_info, type(obj).__name__, id(obj)
)
def _ellipsize(s: str, maxsize: int) -> str:
if len(s) > maxsize:
i = max(0, (maxsize - 3) // 2)
j = max(0, maxsize - 3 - i)
return s[:i] + "..." + s[len(s) - j :]
return s
class SafeRepr(reprlib.Repr):
"""
repr.Repr that limits the resulting size of repr() and includes
information on exceptions raised during the call.
"""
def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
"""
:param maxsize:
If not None, will truncate the resulting repr to that specific size, using ellipsis
somewhere in the middle to hide the extra text.
If None, will not impose any size limits on the returning repr.
"""
super().__init__()
# ``maxstring`` is used by the superclass, and needs to be an int; using a
# very large number in case maxsize is None, meaning we want to disable
# truncation.
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
self.maxsize = maxsize
self.use_ascii = use_ascii
def repr(self, x: object) -> str:
try:
if self.use_ascii:
s = ascii(x)
else:
s = super().repr(x)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s
def repr_instance(self, x: object, level: int) -> str:
try:
s = repr(x)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s
def safeformat(obj: object) -> str:
"""Return a pretty printed string for the given object.
Failing __repr__ functions of user instances will be represented
with a short exception info.
"""
try:
return pprint.pformat(obj)
except Exception as exc:
return _format_repr_exception(exc, obj)
# Maximum size of overall repr of objects to display during assertion errors.
DEFAULT_REPR_MAX_SIZE = 240
def saferepr(
obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
) -> str:
"""Return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented
with a short exception info and 'saferepr' generally takes
care to never raise exceptions itself.
This function is a wrapper around the Repr/reprlib functionality of the
stdlib.
"""
return SafeRepr(maxsize, use_ascii).repr(obj)
def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
"""Return an unlimited-size safe repr-string for the given object.
As with saferepr, failing __repr__ functions of user instances
will be represented with a short exception info.
This function is a wrapper around simple repr.
Note: a cleaner solution would be to alter ``saferepr``this way
when maxsize=None, but that might affect some other code.
"""
try:
if use_ascii:
return ascii(obj)
return repr(obj)
except Exception as exc:
return _format_repr_exception(exc, obj)
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
"""PrettyPrinter that always dispatches (regardless of width)."""
def _format(
self,
object: object,
stream: IO[str],
indent: int,
allowance: int,
context: Dict[int, Any],
level: int,
) -> None:
# Type ignored because _dispatch is private.
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
objid = id(object)
if objid in context or p is None:
# Type ignored because _format is private.
super()._format( # type: ignore[misc]
object,
stream,
indent,
allowance,
context,
level,
)
return
context[objid] = 1
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
def _pformat_dispatch(
object: object,
indent: int = 1,
width: int = 80,
depth: Optional[int] = None,
*,
compact: bool = False,
) -> str:
return AlwaysDispatchingPrettyPrinter(
indent=indent, width=width, depth=depth, compact=compact
).pformat(object)

View File

@ -1,233 +0,0 @@
"""Helper functions for writing to terminals and files."""
import os
import shutil
import sys
from typing import Optional
from typing import Sequence
from typing import TextIO
from .wcwidth import wcswidth
from _pytest.compat import final
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
def get_terminal_width() -> int:
width, _ = shutil.get_terminal_size(fallback=(80, 24))
# The Windows get_terminal_size may be bogus, let's sanify a bit.
if width < 40:
width = 80
return width
def should_do_markup(file: TextIO) -> bool:
if os.environ.get("PY_COLORS") == "1":
return True
if os.environ.get("PY_COLORS") == "0":
return False
if "NO_COLOR" in os.environ:
return False
if "FORCE_COLOR" in os.environ:
return True
return (
hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
)
@final
class TerminalWriter:
_esctable = dict(
black=30,
red=31,
green=32,
yellow=33,
blue=34,
purple=35,
cyan=36,
white=37,
Black=40,
Red=41,
Green=42,
Yellow=43,
Blue=44,
Purple=45,
Cyan=46,
White=47,
bold=1,
light=2,
blink=5,
invert=7,
)
def __init__(self, file: Optional[TextIO] = None) -> None:
if file is None:
file = sys.stdout
if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
try:
import colorama
except ImportError:
pass
else:
file = colorama.AnsiToWin32(file).stream
assert file is not None
self._file = file
self.hasmarkup = should_do_markup(file)
self._current_line = ""
self._terminal_width: Optional[int] = None
self.code_highlight = True
@property
def fullwidth(self) -> int:
if self._terminal_width is not None:
return self._terminal_width
return get_terminal_width()
@fullwidth.setter
def fullwidth(self, value: int) -> None:
self._terminal_width = value
@property
def width_of_current_line(self) -> int:
"""Return an estimate of the width so far in the current line."""
return wcswidth(self._current_line)
def markup(self, text: str, **markup: bool) -> str:
for name in markup:
if name not in self._esctable:
raise ValueError(f"unknown markup: {name!r}")
if self.hasmarkup:
esc = [self._esctable[name] for name, on in markup.items() if on]
if esc:
text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m"
return text
def sep(
self,
sepchar: str,
title: Optional[str] = None,
fullwidth: Optional[int] = None,
**markup: bool,
) -> None:
if fullwidth is None:
fullwidth = self.fullwidth
# The goal is to have the line be as long as possible
# under the condition that len(line) <= fullwidth.
if sys.platform == "win32":
# If we print in the last column on windows we are on a
# new line but there is no way to verify/neutralize this
# (we may not know the exact line width).
# So let's be defensive to avoid empty lines in the output.
fullwidth -= 1
if title is not None:
# we want 2 + 2*len(fill) + len(title) <= fullwidth
# i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
# 2*len(sepchar)*N <= fullwidth - len(title) - 2
# N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
fill = sepchar * N
line = f"{fill} {title} {fill}"
else:
# we want len(sepchar)*N <= fullwidth
# i.e. N <= fullwidth // len(sepchar)
line = sepchar * (fullwidth // len(sepchar))
# In some situations there is room for an extra sepchar at the right,
# in particular if we consider that with a sepchar like "_ " the
# trailing space is not important at the end of the line.
if len(line) + len(sepchar.rstrip()) <= fullwidth:
line += sepchar.rstrip()
self.line(line, **markup)
def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
if msg:
current_line = msg.rsplit("\n", 1)[-1]
if "\n" in msg:
self._current_line = current_line
else:
self._current_line += current_line
msg = self.markup(msg, **markup)
try:
self._file.write(msg)
except UnicodeEncodeError:
# Some environments don't support printing general Unicode
# strings, due to misconfiguration or otherwise; in that case,
# print the string escaped to ASCII.
# When the Unicode situation improves we should consider
# letting the error propagate instead of masking it (see #7475
# for one brief attempt).
msg = msg.encode("unicode-escape").decode("ascii")
self._file.write(msg)
if flush:
self.flush()
def line(self, s: str = "", **markup: bool) -> None:
self.write(s, **markup)
self.write("\n")
def flush(self) -> None:
self._file.flush()
def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
"""Write lines of source code possibly highlighted.
Keeping this private for now because the API is clunky. We should discuss how
to evolve the terminal writer so we can have more precise color support, for example
being able to write part of a line in one color and the rest in another, and so on.
"""
if indents and len(indents) != len(lines):
raise ValueError(
"indents size ({}) should have same size as lines ({})".format(
len(indents), len(lines)
)
)
if not indents:
indents = [""] * len(lines)
source = "\n".join(lines)
new_lines = self._highlight(source).splitlines()
for indent, new_line in zip(indents, new_lines):
self.line(indent + new_line)
def _highlight(self, source: str) -> str:
"""Highlight the given source code if we have markup support."""
from _pytest.config.exceptions import UsageError
if not self.hasmarkup or not self.code_highlight:
return source
try:
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers.python import PythonLexer
from pygments import highlight
import pygments.util
except ImportError:
return source
else:
try:
highlighted: str = highlight(
source,
PythonLexer(),
TerminalFormatter(
bg=os.getenv("PYTEST_THEME_MODE", "dark"),
style=os.getenv("PYTEST_THEME"),
),
)
return highlighted
except pygments.util.ClassNotFound:
raise UsageError(
"PYTEST_THEME environment variable had an invalid value: '{}'. "
"Only valid pygment styles are allowed.".format(
os.getenv("PYTEST_THEME")
)
)
except pygments.util.OptionError:
raise UsageError(
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
"The only allowed values are 'dark' and 'light'.".format(
os.getenv("PYTEST_THEME_MODE")
)
)

View File

@ -1,55 +0,0 @@
import unicodedata
from functools import lru_cache
@lru_cache(100)
def wcwidth(c: str) -> int:
"""Determine how many columns are needed to display a character in a terminal.
Returns -1 if the character is not printable.
Returns 0, 1 or 2 for other characters.
"""
o = ord(c)
# ASCII fast path.
if 0x20 <= o < 0x07F:
return 1
# Some Cf/Zp/Zl characters which should be zero-width.
if (
o == 0x0000
or 0x200B <= o <= 0x200F
or 0x2028 <= o <= 0x202E
or 0x2060 <= o <= 0x2063
):
return 0
category = unicodedata.category(c)
# Control characters.
if category == "Cc":
return -1
# Combining characters with zero width.
if category in ("Me", "Mn"):
return 0
# Full/Wide east asian characters.
if unicodedata.east_asian_width(c) in ("F", "W"):
return 2
return 1
def wcswidth(s: str) -> int:
"""Determine how many columns are needed to display a string in a terminal.
Returns -1 if the string contains non-printable characters.
"""
width = 0
for c in unicodedata.normalize("NFC", s):
wc = wcwidth(c)
if wc < 0:
return -1
width += wc
return width

View File

@ -1,109 +0,0 @@
"""create errno-specific classes for IO or os calls."""
from __future__ import annotations
import errno
import os
import sys
from typing import Callable
from typing import TYPE_CHECKING
from typing import TypeVar
if TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
class Error(EnvironmentError):
def __repr__(self) -> str:
return "{}.{} {!r}: {} ".format(
self.__class__.__module__,
self.__class__.__name__,
self.__class__.__doc__,
" ".join(map(str, self.args)),
# repr(self.args)
)
def __str__(self) -> str:
s = "[{}]: {}".format(
self.__class__.__doc__,
" ".join(map(str, self.args)),
)
return s
_winerrnomap = {
2: errno.ENOENT,
3: errno.ENOENT,
17: errno.EEXIST,
18: errno.EXDEV,
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
22: errno.ENOTDIR,
20: errno.ENOTDIR,
267: errno.ENOTDIR,
5: errno.EACCES, # anything better?
}
class ErrorMaker:
"""lazily provides Exception classes for each possible POSIX errno
(as defined per the 'errno' module). All such instances
subclass EnvironmentError.
"""
_errno2class: dict[int, type[Error]] = {}
def __getattr__(self, name: str) -> type[Error]:
if name[0] == "_":
raise AttributeError(name)
eno = getattr(errno, name)
cls = self._geterrnoclass(eno)
setattr(self, name, cls)
return cls
def _geterrnoclass(self, eno: int) -> type[Error]:
try:
return self._errno2class[eno]
except KeyError:
clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,))
errorcls = type(
clsname,
(Error,),
{"__module__": "py.error", "__doc__": os.strerror(eno)},
)
self._errno2class[eno] = errorcls
return errorcls
def checked_call(
self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
) -> R:
"""Call a function and raise an errno-exception if applicable."""
__tracebackhide__ = True
try:
return func(*args, **kwargs)
except Error:
raise
except OSError as value:
if not hasattr(value, "errno"):
raise
errno = value.errno
if sys.platform == "win32":
try:
cls = self._geterrnoclass(_winerrnomap[errno])
except KeyError:
raise value
else:
# we are not on Windows, or we got a proper OSError
cls = self._geterrnoclass(errno)
raise cls(f"{func.__name__}{args!r}")
_error_maker = ErrorMaker()
checked_call = _error_maker.checked_call
def __getattr__(attr: str) -> type[Error]:
return getattr(_error_maker, attr) # type: ignore[no-any-return]

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
# coding: utf-8
# file generated by setuptools_scm
# don't change, don't track in version control
__version__ = version = '7.2.0'
__version_tuple__ = version_tuple = (7, 2, 0)

View File

@ -1,181 +0,0 @@
"""Support for presenting detailed information in failing assertions."""
import sys
from typing import Any
from typing import Generator
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from _pytest.assertion import rewrite
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.assertion.rewrite import assertstate_key
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
if TYPE_CHECKING:
from _pytest.main import Session
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("debugconfig")
group.addoption(
"--assert",
action="store",
dest="assertmode",
choices=("rewrite", "plain"),
default="rewrite",
metavar="MODE",
help=(
"Control assertion debugging tools.\n"
"'plain' performs no assertion debugging.\n"
"'rewrite' (the default) rewrites assert statements in test modules"
" on import to provide assert expression information."
),
)
parser.addini(
"enable_assertion_pass_hook",
type="bool",
default=False,
help="Enables the pytest_assertion_pass hook. "
"Make sure to delete any previously generated pyc cache files.",
)
def register_assert_rewrite(*names: str) -> None:
"""Register one or more module names to be rewritten on import.
This function will make sure that this module or all modules inside
the package will get their assert statements rewritten.
Thus you should make sure to call this before the module is
actually imported, usually in your __init__.py if you are a plugin
using a package.
:param names: The module names to register.
"""
for name in names:
if not isinstance(name, str):
msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
raise TypeError(msg.format(repr(names)))
for hook in sys.meta_path:
if isinstance(hook, rewrite.AssertionRewritingHook):
importhook = hook
break
else:
# TODO(typing): Add a protocol for mark_rewrite() and use it
# for importhook and for PytestPluginManager.rewrite_hook.
importhook = DummyRewriteHook() # type: ignore
importhook.mark_rewrite(*names)
class DummyRewriteHook:
"""A no-op import hook for when rewriting is disabled."""
def mark_rewrite(self, *names: str) -> None:
pass
class AssertionState:
"""State for the assertion plugin."""
def __init__(self, config: Config, mode) -> None:
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.hook: Optional[rewrite.AssertionRewritingHook] = None
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
"""Try to install the rewrite hook, raise SystemError if it fails."""
config.stash[assertstate_key] = AssertionState(config, "rewrite")
config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
sys.meta_path.insert(0, hook)
config.stash[assertstate_key].trace("installed rewrite import hook")
def undo() -> None:
hook = config.stash[assertstate_key].hook
if hook is not None and hook in sys.meta_path:
sys.meta_path.remove(hook)
config.add_cleanup(undo)
return hook
def pytest_collection(session: "Session") -> None:
# This hook is only called when test modules are collected
# so for example not in the managing process of pytest-xdist
# (which does not collect test modules).
assertstate = session.config.stash.get(assertstate_key, None)
if assertstate:
if assertstate.hook is not None:
assertstate.hook.set_session(session)
@hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
The rewrite module will use util._reprcompare if it exists to use custom
reporting via the pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:
"""Call the pytest_assertrepr_compare hook and prepare the result.
This uses the first result from the hook and then ensures the
following:
* Overly verbose explanations are truncated unless configured otherwise
(eg. if running in verbose mode).
* Embedded newlines are escaped to help util.format_explanation()
later.
* If the rewrite mode is used embedded %-characters are replaced
to protect later % formatting.
The result can be formatted by util.format_explanation() for
pretty printing.
"""
hook_result = ihook.pytest_assertrepr_compare(
config=item.config, op=op, left=left, right=right
)
for new_expl in hook_result:
if new_expl:
new_expl = truncate.truncate_if_required(new_expl, item)
new_expl = [line.replace("\n", "\\n") for line in new_expl]
res = "\n~".join(new_expl)
if item.config.getvalue("assertmode") == "rewrite":
res = res.replace("%", "%%")
return res
return None
saved_assert_hooks = util._reprcompare, util._assertion_pass
util._reprcompare = callbinrepr
util._config = item.config
if ihook.pytest_assertion_pass.get_hookimpls():
def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
util._assertion_pass = call_assertion_pass_hook
yield
util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None
def pytest_sessionfinish(session: "Session") -> None:
assertstate = session.config.stash.get(assertstate_key, None)
if assertstate:
if assertstate.hook is not None:
assertstate.hook.set_session(None)
def pytest_assertrepr_compare(
config: Config, op: str, left: Any, right: Any
) -> Optional[List[str]]:
return util.assertrepr_compare(config=config, op=op, left=left, right=right)

View File

@ -1,94 +0,0 @@
"""Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI.
"""
from typing import List
from typing import Optional
from _pytest.assertion import util
from _pytest.nodes import Item
DEFAULT_MAX_LINES = 8
DEFAULT_MAX_CHARS = 8 * 80
USAGE_MSG = "use '-vv' to show"
def truncate_if_required(
explanation: List[str], item: Item, max_length: Optional[int] = None
) -> List[str]:
"""Truncate this assertion explanation if the given test item is eligible."""
if _should_truncate_item(item):
return _truncate_explanation(explanation)
return explanation
def _should_truncate_item(item: Item) -> bool:
"""Whether or not this test item is eligible for truncation."""
verbose = item.config.option.verbose
return verbose < 2 and not util.running_on_ci()
def _truncate_explanation(
input_lines: List[str],
max_lines: Optional[int] = None,
max_chars: Optional[int] = None,
) -> List[str]:
"""Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message.
"""
if max_lines is None:
max_lines = DEFAULT_MAX_LINES
if max_chars is None:
max_chars = DEFAULT_MAX_CHARS
# Check if truncation required
input_char_count = len("".join(input_lines))
if len(input_lines) <= max_lines and input_char_count <= max_chars:
return input_lines
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
# Append useful message to explanation
truncated_line_count = len(input_lines) - len(truncated_explanation)
truncated_line_count += 1 # Account for the part-truncated final line
msg = "...Full output truncated"
if truncated_line_count == 1:
msg += f" ({truncated_line_count} line hidden)"
else:
msg += f" ({truncated_line_count} lines hidden)"
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)])
return truncated_explanation
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
# Check if truncation required
if len("".join(input_lines)) <= max_chars:
return input_lines
# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):
if iterated_char_count + len(input_line) > max_chars:
break
iterated_char_count += len(input_line)
# Create truncated explanation with modified final line
truncated_result = input_lines[:iterated_index]
final_line = input_lines[iterated_index]
if final_line:
final_line_truncate_point = max_chars - iterated_char_count
final_line = final_line[:final_line_truncate_point]
truncated_result.append(final_line)
return truncated_result

View File

@ -1,522 +0,0 @@
"""Utilities for assertion debugging."""
import collections.abc
import os
import pprint
from typing import AbstractSet
from typing import Any
from typing import Callable
from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from unicodedata import normalize
import _pytest._code
from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
# Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called.
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
# Config object which is assigned during pytest_runtest_protocol.
_config: Optional[Config] = None
def format_explanation(explanation: str) -> str:
r"""Format an explanation.
Normally all embedded newlines are escaped, however there are
three exceptions: \n{, \n} and \n~. The first two are intended
cover nested explanations, see function and attribute explanations
for examples (.visit_Call(), visit_Attribute()). The last one is
for when one explanation needs to span multiple lines, e.g. when
displaying diffs.
"""
lines = _split_explanation(explanation)
result = _format_lines(lines)
return "\n".join(result)
def _split_explanation(explanation: str) -> List[str]:
r"""Return a list of individual lines in the explanation.
This will return a list of lines split on '\n{', '\n}' and '\n~'.
Any other newlines will be escaped and appear in the line as the
literal '\n' characters.
"""
raw_lines = (explanation or "").split("\n")
lines = [raw_lines[0]]
for values in raw_lines[1:]:
if values and values[0] in ["{", "}", "~", ">"]:
lines.append(values)
else:
lines[-1] += "\\n" + values
return lines
def _format_lines(lines: Sequence[str]) -> List[str]:
"""Format the individual lines.
This will replace the '{', '}' and '~' characters of our mini formatting
language with the proper 'where ...', 'and ...' and ' + ...' text, taking
care of indentation along the way.
Return a list of formatted lines.
"""
result = list(lines[:1])
stack = [0]
stackcnt = [0]
for line in lines[1:]:
if line.startswith("{"):
if stackcnt[-1]:
s = "and "
else:
s = "where "
stack.append(len(result))
stackcnt[-1] += 1
stackcnt.append(0)
result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
elif line.startswith("}"):
stack.pop()
stackcnt.pop()
result[stack[-1]] += line[1:]
else:
assert line[0] in ["~", ">"]
stack[-1] += 1
indent = len(stack) if line.startswith("~") else len(stack) - 1
result.append(" " * indent + line[1:])
assert len(stack) == 1
return result
def issequence(x: Any) -> bool:
return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
def istext(x: Any) -> bool:
return isinstance(x, str)
def isdict(x: Any) -> bool:
return isinstance(x, dict)
def isset(x: Any) -> bool:
return isinstance(x, (set, frozenset))
def isnamedtuple(obj: Any) -> bool:
return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
def isdatacls(obj: Any) -> bool:
return getattr(obj, "__dataclass_fields__", None) is not None
def isattrs(obj: Any) -> bool:
return getattr(obj, "__attrs_attrs__", None) is not None
def isiterable(obj: Any) -> bool:
try:
iter(obj)
return not istext(obj)
except TypeError:
return False
def has_default_eq(
obj: object,
) -> bool:
"""Check if an instance of an object contains the default eq
First, we check if the object's __eq__ attribute has __code__,
if so, we check the equally of the method code filename (__code__.co_filename)
to the default one generated by the dataclass and attr module
for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
"""
# inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
code_filename = obj.__eq__.__code__.co_filename
if isattrs(obj):
return "attrs generated eq" in code_filename
return code_filename == "<string>" # data class
return True
def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
use_ascii = (
isinstance(left, str)
and isinstance(right, str)
and normalize("NFD", left) == normalize("NFD", right)
)
if verbose > 1:
left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
else:
# XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width.
maxsize = (
80 - 15 - len(op) - 2
) // 2 # 15 chars indentation, 1 space around op
left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
summary = f"{left_repr} {op} {right_repr}"
explanation = None
try:
if op == "==":
explanation = _compare_eq_any(left, right, verbose)
elif op == "not in":
if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose)
except outcomes.Exit:
raise
except Exception:
explanation = [
"(pytest_assertion plugin: representation of details failed: {}.".format(
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
),
" Probably an object has a faulty __repr__.)",
]
if not explanation:
return None
return [summary] + explanation
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
explanation = []
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
else:
from _pytest.python_api import ApproxBase
if isinstance(left, ApproxBase) or isinstance(right, ApproxBase):
# Although the common order should be obtained == expected, this ensures both ways
approx_side = left if isinstance(left, ApproxBase) else right
other_side = right if isinstance(left, ApproxBase) else left
explanation = approx_side._repr_compare(other_side)
elif type(left) == type(right) and (
isdatacls(left) or isattrs(left) or isnamedtuple(left)
):
# Note: unlike dataclasses/attrs, namedtuples compare only the
# field values, not the type or field names. But this branch
# intentionally only handles the same-type case, which was often
# used in older code bases before dataclasses/attrs were available.
explanation = _compare_eq_cls(left, right, verbose)
elif issequence(left) and issequence(right):
explanation = _compare_eq_sequence(left, right, verbose)
elif isset(left) and isset(right):
explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose)
if isiterable(left) and isiterable(right):
expl = _compare_eq_iterable(left, right, verbose)
explanation.extend(expl)
return explanation
def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
"""Return the explanation for the diff between text.
Unless --verbose is used this will skip leading and trailing
characters which are identical to keep the diff minimal.
"""
from difflib import ndiff
explanation: List[str] = []
if verbose < 1:
i = 0 # just in case left or right has zero length
for i in range(min(len(left), len(right))):
if left[i] != right[i]:
break
if i > 42:
i -= 10 # Provide some context
explanation = [
"Skipping %s identical leading characters in diff, use -v to show" % i
]
left = left[i:]
right = right[i:]
if len(left) == len(right):
for i in range(len(left)):
if left[-i] != right[-i]:
break
if i > 42:
i -= 10 # Provide some context
explanation += [
"Skipping {} identical trailing "
"characters in diff, use -v to show".format(i)
]
left = left[:-i]
right = right[:-i]
keepends = True
if left.isspace() or right.isspace():
left = repr(str(left))
right = repr(str(right))
explanation += ["Strings contain only whitespace, escaping them using repr()"]
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation += [
line.strip("\n")
for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
]
return explanation
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
"""Move opening/closing parenthesis/bracket to own lines."""
opening = lines[0][:1]
if opening in ["(", "[", "{"]:
lines[0] = " " + lines[0][1:]
lines[:] = [opening] + lines
closing = lines[-1][-1:]
if closing in [")", "]", "}"]:
lines[-1] = lines[-1][:-1] + ","
lines[:] = lines + [closing]
def _compare_eq_iterable(
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
) -> List[str]:
if verbose <= 0 and not running_on_ci():
return ["Use -v to get more diff"]
# dynamic import to speedup pytest
import difflib
left_formatting = pprint.pformat(left).splitlines()
right_formatting = pprint.pformat(right).splitlines()
# Re-format for different output lengths.
lines_left = len(left_formatting)
lines_right = len(right_formatting)
if lines_left != lines_right:
left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = _pformat_dispatch(right).splitlines()
if lines_left > 1 or lines_right > 1:
_surrounding_parens_on_own_lines(left_formatting)
_surrounding_parens_on_own_lines(right_formatting)
explanation = ["Full diff:"]
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation.extend(
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
)
return explanation
def _compare_eq_sequence(
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
) -> List[str]:
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation: List[str] = []
len_left = len(left)
len_right = len(right)
for i in range(min(len_left, len_right)):
if left[i] != right[i]:
if comparing_bytes:
# when comparing bytes, we want to see their ascii representation
# instead of their numeric values (#5260)
# using a slice gives us the ascii representation:
# >>> s = b'foo'
# >>> s[0]
# 102
# >>> s[0:1]
# b'f'
left_value = left[i : i + 1]
right_value = right[i : i + 1]
else:
left_value = left[i]
right_value = right[i]
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
break
if comparing_bytes:
# when comparing bytes, it doesn't help to show the "sides contain one or more
# items" longer explanation, so skip it
return explanation
len_diff = len_left - len_right
if len_diff:
if len_diff > 0:
dir_with_more = "Left"
extra = saferepr(left[len_right])
else:
len_diff = 0 - len_diff
dir_with_more = "Right"
extra = saferepr(right[len_left])
if len_diff == 1:
explanation += [f"{dir_with_more} contains one more item: {extra}"]
else:
explanation += [
"%s contains %d more items, first extra item: %s"
% (dir_with_more, len_diff, extra)
]
return explanation
def _compare_eq_set(
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
) -> List[str]:
explanation = []
diff_left = left - right
diff_right = right - left
if diff_left:
explanation.append("Extra items in the left set:")
for item in diff_left:
explanation.append(saferepr(item))
if diff_right:
explanation.append("Extra items in the right set:")
for item in diff_right:
explanation.append(saferepr(item))
return explanation
def _compare_eq_dict(
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
) -> List[str]:
explanation: List[str] = []
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)
same = {k: left[k] for k in common if left[k] == right[k]}
if same and verbose < 2:
explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
elif same:
explanation += ["Common items:"]
explanation += pprint.pformat(same).splitlines()
diff = {k for k in common if left[k] != right[k]}
if diff:
explanation += ["Differing items:"]
for k in diff:
explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
extra_left = set_left - set_right
len_extra_left = len(extra_left)
if len_extra_left:
explanation.append(
"Left contains %d more item%s:"
% (len_extra_left, "" if len_extra_left == 1 else "s")
)
explanation.extend(
pprint.pformat({k: left[k] for k in extra_left}).splitlines()
)
extra_right = set_right - set_left
len_extra_right = len(extra_right)
if len_extra_right:
explanation.append(
"Right contains %d more item%s:"
% (len_extra_right, "" if len_extra_right == 1 else "s")
)
explanation.extend(
pprint.pformat({k: right[k] for k in extra_right}).splitlines()
)
return explanation
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if not has_default_eq(left):
return []
if isdatacls(left):
import dataclasses
all_fields = dataclasses.fields(left)
fields_to_check = [info.name for info in all_fields if info.compare]
elif isattrs(left):
all_fields = left.__attrs_attrs__
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
elif isnamedtuple(left):
fields_to_check = left._fields
else:
assert False
indent = " "
same = []
diff = []
for field in fields_to_check:
if getattr(left, field) == getattr(right, field):
same.append(field)
else:
diff.append(field)
explanation = []
if same or diff:
explanation += [""]
if same and verbose < 2:
explanation.append("Omitting %s identical items, use -vv to show" % len(same))
elif same:
explanation += ["Matching attributes:"]
explanation += pprint.pformat(same).splitlines()
if diff:
explanation += ["Differing attributes:"]
explanation += pprint.pformat(diff).splitlines()
for field in diff:
field_left = getattr(left, field)
field_right = getattr(right, field)
explanation += [
"",
"Drill down into differing attribute %s:" % field,
("%s%s: %r != %r") % (indent, field, field_left, field_right),
]
explanation += [
indent + line
for line in _compare_eq_any(field_left, field_right, verbose)
]
return explanation
def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
index = text.find(term)
head = text[:index]
tail = text[index + len(term) :]
correct_text = head + tail
diff = _diff_text(text, correct_text, verbose)
newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
for line in diff:
if line.startswith("Skipping"):
continue
if line.startswith("- "):
continue
if line.startswith("+ "):
newdiff.append(" " + line[2:])
else:
newdiff.append(line)
return newdiff
def running_on_ci() -> bool:
"""Check if we're currently running on a CI system."""
env_vars = ["CI", "BUILD_NUMBER"]
return any(var in os.environ for var in env_vars)

View File

@ -1,580 +0,0 @@
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import json
import os
from pathlib import Path
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Union
import attr
from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
from _pytest.python import Module
from _pytest.python import Package
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
This directory contains data from the pytest's cache plugin,
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
**Do not** commit this to version control.
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
"""
CACHEDIR_TAG_CONTENT = b"""\
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by pytest.
# For information about cache directory tags, see:
# https://bford.info/cachedir/spec.html
"""
@final
@attr.s(init=False, auto_attribs=True)
class Cache:
_cachedir: Path = attr.ib(repr=False)
_config: Config = attr.ib(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d"
# Sub-directory under cache-dir for values created by `set()`.
_CACHE_PREFIX_VALUES = "v"
def __init__(
self, cachedir: Path, config: Config, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._cachedir = cachedir
self._config = config
@classmethod
def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
"""Create the Cache instance for a Config.
:meta private:
"""
check_ispytest(_ispytest)
cachedir = cls.cache_dir_from_config(config, _ispytest=True)
if config.getoption("cacheclear") and cachedir.is_dir():
cls.clear_cache(cachedir, _ispytest=True)
return cls(cachedir, config, _ispytest=True)
@classmethod
def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
"""Clear the sub-directories used to hold cached directories and values.
:meta private:
"""
check_ispytest(_ispytest)
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
d = cachedir / prefix
if d.is_dir():
rm_rf(d)
@staticmethod
def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
"""Get the path to the cache directory for a Config.
:meta private:
"""
check_ispytest(_ispytest)
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
"""Issue a cache warning.
:meta private:
"""
check_ispytest(_ispytest)
import warnings
from _pytest.warning_types import PytestCacheWarning
warnings.warn(
PytestCacheWarning(fmt.format(**args) if args else fmt),
self._config.hook,
stacklevel=3,
)
def mkdir(self, name: str) -> Path:
"""Return a directory path object with the given name.
If the directory does not yet exist, it will be created. You can use
it to manage files to e.g. store/retrieve database dumps across test
sessions.
.. versionadded:: 7.0
:param name:
Must be a string not containing a ``/`` separator.
Make sure the name contains your plugin or application
identifiers to prevent clashes with other cache users.
"""
path = Path(name)
if len(path.parts) > 1:
raise ValueError("name is not allowed to contain path separators")
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
res.mkdir(exist_ok=True, parents=True)
return res
def _getvaluepath(self, key: str) -> Path:
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
def get(self, key: str, default):
"""Return the cached value for the given key.
If no value was yet cached or the value cannot be read, the specified
default is returned.
:param key:
Must be a ``/`` separated value. Usually the first
name is the name of your plugin or your application.
:param default:
The value to return in case of a cache-miss or invalid cache value.
"""
path = self._getvaluepath(key)
try:
with path.open("r", encoding="UTF-8") as f:
return json.load(f)
except (ValueError, OSError):
return default
def set(self, key: str, value: object) -> None:
"""Save value for the given key.
:param key:
Must be a ``/`` separated value. Usually the first
name is the name of your plugin or your application.
:param value:
Must be of any combination of basic python types,
including nested types like lists of dictionaries.
"""
path = self._getvaluepath(key)
try:
if path.parent.is_dir():
cache_dir_exists_already = True
else:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError:
self.warn("could not create cache path {path}", path=path, _ispytest=True)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2)
try:
f = path.open("w", encoding="UTF-8")
except OSError:
self.warn("cache could not write path {path}", path=path, _ispytest=True)
else:
with f:
f.write(data)
def _ensure_supporting_files(self) -> None:
"""Create supporting files in the cache dir that are not really part of the cache."""
readme_path = self._cachedir / "README.md"
readme_path.write_text(README_CONTENT, encoding="UTF-8")
gitignore_path = self._cachedir.joinpath(".gitignore")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path.write_text(msg, encoding="UTF-8")
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
class LFPluginCollWrapper:
def __init__(self, lfplugin: "LFPlugin") -> None:
self.lfplugin = lfplugin
self._collected_at_least_one_failure = False
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
res.result = sorted(
res.result,
# use stable sort to priorize last failed
key=lambda x: x.path in lf_paths,
reverse=True,
)
return
elif isinstance(collector, Module):
if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
result = res.result
lastfailed = self.lfplugin.lastfailed
# Only filter with known failures.
if not self._collected_at_least_one_failure:
if not any(x.nodeid in lastfailed for x in result):
return
self.lfplugin.config.pluginmanager.register(
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
)
self._collected_at_least_one_failure = True
session = collector.session
result[:] = [
x
for x in result
if x.nodeid in lastfailed
# Include any passed arguments (not trivial to filter).
or session.isinitpath(x.path)
# Keep all sub-collectors.
or isinstance(x, nodes.Collector)
]
return
yield
class LFPluginCollSkipfiles:
def __init__(self, lfplugin: "LFPlugin") -> None:
self.lfplugin = lfplugin
@hookimpl
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains
# test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1
return CollectReport(
collector.nodeid, "passed", longrepr=None, result=[]
)
return None
class LFPlugin:
"""Plugin which implements the --lf (run last-failing) option."""
def __init__(self, config: Config) -> None:
self.config = config
active_keys = "lf", "failedfirst"
self.active = any(config.getoption(key) for key in active_keys)
assert config.cache
self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
self._previously_failed_count: Optional[int] = None
self._report_status: Optional[str] = None
self._skipped_files = 0 # count skipped files during collection due to --lf
if config.getoption("lf"):
self._last_failed_paths = self.get_last_failed_paths()
config.pluginmanager.register(
LFPluginCollWrapper(self), "lfplugin-collwrapper"
)
def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids."""
rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]:
if self.active and self.config.getoption("verbose") >= 0:
return "run-last-failure: %s" % self._report_status
return None
def pytest_runtest_logreport(self, report: TestReport) -> None:
if (report.when == "call" and report.passed) or report.skipped:
self.lastfailed.pop(report.nodeid, None)
elif report.failed:
self.lastfailed[report.nodeid] = True
def pytest_collectreport(self, report: CollectReport) -> None:
passed = report.outcome in ("passed", "skipped")
if passed:
if report.nodeid in self.lastfailed:
self.lastfailed.pop(report.nodeid)
self.lastfailed.update((item.nodeid, True) for item in report.result)
else:
self.lastfailed[report.nodeid] = True
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item]
) -> Generator[None, None, None]:
yield
if not self.active:
return
if self.lastfailed:
previously_failed = []
previously_passed = []
for item in items:
if item.nodeid in self.lastfailed:
previously_failed.append(item)
else:
previously_passed.append(item)
self._previously_failed_count = len(previously_failed)
if not previously_failed:
# Running a subset of all tests with recorded failures
# only outside of it.
self._report_status = "%d known failures not in selected tests" % (
len(self.lastfailed),
)
else:
if self.config.getoption("lf"):
items[:] = previously_failed
config.hook.pytest_deselected(items=previously_passed)
else: # --failedfirst
items[:] = previously_failed + previously_passed
noun = "failure" if self._previously_failed_count == 1 else "failures"
suffix = " first" if self.config.getoption("failedfirst") else ""
self._report_status = "rerun previous {count} {noun}{suffix}".format(
count=self._previously_failed_count, suffix=suffix, noun=noun
)
if self._skipped_files > 0:
files_noun = "file" if self._skipped_files == 1 else "files"
self._report_status += " (skipped {files} {files_noun})".format(
files=self._skipped_files, files_noun=files_noun
)
else:
self._report_status = "no previously failed tests, "
if self.config.getoption("last_failed_no_failures") == "none":
self._report_status += "deselecting all items."
config.hook.pytest_deselected(items=items[:])
items[:] = []
else:
self._report_status += "not deselecting items."
def pytest_sessionfinish(self, session: Session) -> None:
config = self.config
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
return
assert config.cache is not None
saved_lastfailed = config.cache.get("cache/lastfailed", {})
if saved_lastfailed != self.lastfailed:
config.cache.set("cache/lastfailed", self.lastfailed)
class NFPlugin:
"""Plugin which implements the --nf (run new-first) option."""
def __init__(self, config: Config) -> None:
self.config = config
self.active = config.option.newfirst
assert config.cache is not None
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, items: List[nodes.Item]
) -> Generator[None, None, None]:
yield
if self.active:
new_items: Dict[str, nodes.Item] = {}
other_items: Dict[str, nodes.Item] = {}
for item in items:
if item.nodeid not in self.cached_nodeids:
new_items[item.nodeid] = item
else:
other_items[item.nodeid] = item
items[:] = self._get_increasing_order(
new_items.values()
) + self._get_increasing_order(other_items.values())
self.cached_nodeids.update(new_items)
else:
self.cached_nodeids.update(item.nodeid for item in items)
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
def pytest_sessionfinish(self) -> None:
config = self.config
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
return
if config.getoption("collectonly"):
return
assert config.cache is not None
config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group.addoption(
"--lf",
"--last-failed",
action="store_true",
dest="lf",
help="Rerun only the tests that failed "
"at the last run (or all if none failed)",
)
group.addoption(
"--ff",
"--failed-first",
action="store_true",
dest="failedfirst",
help="Run all tests, but run the last failures first. "
"This may re-order tests and thus lead to "
"repeated fixture setup/teardown.",
)
group.addoption(
"--nf",
"--new-first",
action="store_true",
dest="newfirst",
help="Run tests from new files first, then the rest of the tests "
"sorted by file mtime",
)
group.addoption(
"--cache-show",
action="append",
nargs="?",
dest="cacheshow",
help=(
"Show cache contents, don't perform collection or tests. "
"Optional argument: glob (default: '*')."
),
)
group.addoption(
"--cache-clear",
action="store_true",
dest="cacheclear",
help="Remove all cache contents at start of test run",
)
cache_dir_default = ".pytest_cache"
if "TOX_ENV_DIR" in os.environ:
cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
group.addoption(
"--lfnf",
"--last-failed-no-failures",
action="store",
dest="last_failed_no_failures",
choices=("all", "none"),
default="all",
help="Which tests to run with no previously (known) failures",
)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.cacheshow:
from _pytest.main import wrap_session
return wrap_session(config, cacheshow)
return None
@hookimpl(tryfirst=True)
def pytest_configure(config: Config) -> None:
config.cache = Cache.for_config(config, _ispytest=True)
config.pluginmanager.register(LFPlugin(config), "lfplugin")
config.pluginmanager.register(NFPlugin(config), "nfplugin")
@fixture
def cache(request: FixtureRequest) -> Cache:
"""Return a cache object that can persist state between testing sessions.
cache.get(key, default)
cache.set(key, value)
Keys must be ``/`` separated strings, where the first part is usually the
name of your plugin or application to avoid clashes with other cache users.
Values can be any object handled by the json stdlib module.
"""
assert request.config.cache is not None
return request.config.cache
def pytest_report_header(config: Config) -> Optional[str]:
"""Display cachedir with --cache-show and if non-default."""
if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
assert config.cache is not None
cachedir = config.cache._cachedir
# TODO: evaluate generating upward relative paths
# starting with .., ../.. if sensible
try:
displaypath = cachedir.relative_to(config.rootpath)
except ValueError:
displaypath = cachedir
return f"cachedir: {displaypath}"
return None
def cacheshow(config: Config, session: Session) -> int:
from pprint import pformat
assert config.cache is not None
tw = TerminalWriter()
tw.line("cachedir: " + str(config.cache._cachedir))
if not config.cache._cachedir.is_dir():
tw.line("cache is empty")
return 0
glob = config.option.cacheshow[0]
if glob is None:
glob = "*"
dummy = object()
basedir = config.cache._cachedir
vdir = basedir / Cache._CACHE_PREFIX_VALUES
tw.sep("-", "cache values for %r" % glob)
for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
key = str(valpath.relative_to(vdir))
val = config.cache.get(key, dummy)
if val is dummy:
tw.line("%s contains unreadable content, will be ignored" % key)
else:
tw.line("%s contains:" % key)
for line in pformat(val).splitlines():
tw.line(" " + line)
ddir = basedir / Cache._CACHE_PREFIX_DIRS
if ddir.is_dir():
contents = sorted(ddir.rglob(glob))
tw.sep("-", "cache directories for %r" % glob)
for p in contents:
# if p.is_dir():
# print("%s/" % p.relative_to(basedir))
if p.is_file():
key = str(p.relative_to(basedir))
tw.line(f"{key} is a file of length {p.stat().st_size:d}")
return 0

File diff suppressed because it is too large Load Diff

View File

@ -1,417 +0,0 @@
"""Python version compatibility code."""
import enum
import functools
import inspect
import os
import sys
from inspect import Parameter
from inspect import signature
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Generic
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import py
# fmt: off
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
# If `overload` is imported from `compat` instead of from `typing`,
# Sphinx doesn't recognize it as `overload` and the API docs for
# overloaded functions look good again. But type checkers handle
# it fine.
# fmt: on
if True:
from typing import overload as overload
if TYPE_CHECKING:
from typing_extensions import Final
_T = TypeVar("_T")
_S = TypeVar("_S")
#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0
# fmt: off
# intentional space to create a fake difference for the verification
LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
# fmt: off
# Singleton type for NOTSET, as described in:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET: "Final" = NotSetType.token # noqa: E305
# fmt: on
if sys.version_info >= (3, 8):
import importlib.metadata
importlib_metadata = importlib.metadata
else:
import importlib_metadata as importlib_metadata # noqa: F401
def _format_args(func: Callable[..., Any]) -> str:
return str(signature(func))
def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func)
return genfunc and not iscoroutinefunction(func)
def iscoroutinefunction(func: object) -> bool:
"""Return True if func is a coroutine function (a function defined with async
def syntax, and doesn't contain yield), or a function decorated with
@asyncio.coroutine.
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
importing asyncio directly, which in turns also initializes the "logging"
module as a side-effect (see issue #8).
"""
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
def is_async_function(func: object) -> bool:
"""Return True if the given function seems to be an async function or
an async generator."""
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str:
function = get_real_func(function)
fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
if curdir is not None:
try:
relfn = fn.relative_to(curdir)
except ValueError:
pass
else:
return "%s:%d" % (relfn, lineno + 1)
return "%s:%d" % (fn, lineno + 1)
def num_mock_patch_args(function) -> int:
"""Return number of arguments used up by mock arguments (if any)."""
patchings = getattr(function, "patchings", None)
if not patchings:
return 0
mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
return len(
[
p
for p in patchings
if not p.attribute_name
and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
]
)
def getfuncargnames(
function: Callable[..., Any],
*,
name: str = "",
is_method: bool = False,
cls: Optional[type] = None,
) -> Tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
Should return the names of all function arguments that:
* Aren't bound to an instance or type as in instance or class methods.
* Don't have default values.
* Aren't bound with functools.partial.
* Aren't replaced with mocks.
The is_method and cls arguments indicate that the function should
be treated as a bound method even though it's not unless, only in
the case of cls, the function is a static method.
The name parameter should be the original name in which the function was collected.
"""
# TODO(RonnyPfannschmidt): This function should be refactored when we
# revisit fixtures. The fixture mechanism should ask the node for
# the fixture names, and not try to obtain directly from the
# function object well after collection has occurred.
# The parameters attribute of a Signature object contains an
# ordered mapping of parameter names to Parameter instances. This
# creates a tuple of the names of the parameters that don't have
# defaults.
try:
parameters = signature(function).parameters
except (ValueError, TypeError) as e:
from _pytest.outcomes import fail
fail(
f"Could not determine arguments of {function!r}: {e}",
pytrace=False,
)
arg_names = tuple(
p.name
for p in parameters.values()
if (
p.kind is Parameter.POSITIONAL_OR_KEYWORD
or p.kind is Parameter.KEYWORD_ONLY
)
and p.default is Parameter.empty
)
if not name:
name = function.__name__
# If this function should be treated as a bound method even though
# it's passed as an unbound method or function, remove the first
# parameter name.
if is_method or (
# Not using `getattr` because we don't want to resolve the staticmethod.
# Not using `cls.__dict__` because we want to check the entire MRO.
cls
and not isinstance(
inspect.getattr_static(cls, name, default=None), staticmethod
)
):
arg_names = arg_names[1:]
# Remove any names that will be replaced with mocks.
if hasattr(function, "__wrapped__"):
arg_names = arg_names[num_mock_patch_args(function) :]
return arg_names
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result
# because they had default values.
return tuple(
p.name
for p in signature(function).parameters.values()
if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
and p.default is not Parameter.empty
)
_non_printable_ascii_translate_table = {
i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
}
_non_printable_ascii_translate_table.update(
{ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
)
def _translate_non_printable(s: str) -> str:
return s.translate(_non_printable_ascii_translate_table)
STRING_TYPES = bytes, str
def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: Union[bytes, str]) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
and escapes unicode objects into a sequence of escaped unicode
ids, e.g.:
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
Note:
The obvious "v.decode('unicode-escape')" will return
valid UTF-8 unicode if it finds them in bytes, but we
want to return escaped bytes for any byte, even if they match
a UTF-8 string.
"""
if isinstance(val, bytes):
ret = _bytes_to_ascii(val)
else:
ret = val.encode("unicode_escape").decode("ascii")
return _translate_non_printable(ret)
@attr.s
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
Used to correctly unwrap the underlying function object when we are
creating fixtures, because we wrap the function object ourselves with a
decorator to issue warnings when the fixture function is called directly.
"""
obj = attr.ib()
def get_real_func(obj):
"""Get the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial."""
start_obj = obj
for i in range(100):
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
# to trigger a warning if it gets called directly instead of by pytest: we don't
# want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
new_obj = getattr(obj, "__pytest_wrapped__", None)
if isinstance(new_obj, _PytestWrapper):
obj = new_obj.obj
break
new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None:
break
obj = new_obj
else:
from _pytest._io.saferepr import saferepr
raise ValueError(
("could not find real function of {start}\nstopped at {current}").format(
start=saferepr(start_obj), current=saferepr(obj)
)
)
if isinstance(obj, functools.partial):
obj = obj.func
return obj
def get_real_method(obj, holder):
"""Attempt to obtain the real function object that might be wrapping
``obj``, while at the same time returning a bound method to ``holder`` if
the original object was a bound method."""
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
except Exception: # pragma: no cover
return obj
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
obj = obj.__get__(holder)
return obj
def getimfunc(func):
try:
return func.__func__
except AttributeError:
return func
def safe_getattr(object: Any, name: str, default: Any) -> Any:
"""Like getattr but return default upon any Exception or any OutcomeException.
Attribute access can potentially fail for 'evil' Python objects.
See issue #214.
It catches OutcomeException because of #2490 (issue #580), new outcomes
are derived from BaseException instead of Exception (for more details
check #2707).
"""
from _pytest.outcomes import TEST_OUTCOME
try:
return getattr(object, name, default)
except TEST_OUTCOME:
return default
def safe_isclass(obj: object) -> bool:
"""Ignore any exception via isinstance on Python 3."""
try:
return inspect.isclass(obj)
except Exception:
return False
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import final as final
else:
from typing_extensions import final as final
elif sys.version_info >= (3, 8):
from typing import final as final
else:
def final(f):
return f
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
from typing import Type
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
def __init__(self, func: Callable[[_S], _T]) -> None:
self.func = func
self.__doc__ = func.__doc__
@overload
def __get__(
self, instance: None, owner: Optional[Type[_S]] = ...
) -> "cached_property[_S, _T]":
...
@overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
...
def __get__(self, instance, owner=None):
if instance is None:
return self
value = instance.__dict__[self.func.__name__] = self.func(instance)
return value
# Perform exhaustiveness checking.
#
# Consider this example:
#
# MyUnion = Union[int, str]
#
# def handle(x: MyUnion) -> int {
# if isinstance(x, int):
# return 1
# elif isinstance(x, str):
# return 2
# else:
# raise Exception('unreachable')
#
# Now suppose we add a new variant:
#
# MyUnion = Union[int, str, bytes]
#
# After doing this, we must remember ourselves to go and update the handle
# function to handle the new variant.
#
# With `assert_never` we can do better:
#
# // raise Exception('unreachable')
# return assert_never(x)
#
# Now, if we forget to handle the new variant, the type-checker will emit a
# compile-time error, instead of the runtime error we would have gotten
# previously.
#
# This also work for Enums (if you use `is` to compare) and Literals.
def assert_never(value: NoReturn) -> NoReturn:
assert False, f"Unhandled value: {value} ({type(value).__name__})"

File diff suppressed because it is too large Load Diff

View File

@ -1,551 +0,0 @@
import argparse
import os
import sys
import warnings
from gettext import gettext
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import List
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import _pytest._io
from _pytest.compat import final
from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest
if TYPE_CHECKING:
from typing_extensions import Literal
FILE_OR_DIR = "file_or_dir"
@final
class Parser:
"""Parser for command line arguments and ini-file values.
:ivar extra_info: Dict of generic param -> value to display in case
there's an error processing the command line arguments.
"""
prog: Optional[str] = None
def __init__(
self,
usage: Optional[str] = None,
processopt: Optional[Callable[["Argument"], None]] = None,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True)
self._groups: List[OptionGroup] = []
self._processopt = processopt
self._usage = usage
self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
self._ininames: List[str] = []
self.extra_info: Dict[str, Any] = {}
def processoption(self, option: "Argument") -> None:
if self._processopt:
if option.dest:
self._processopt(option)
def getgroup(
self, name: str, description: str = "", after: Optional[str] = None
) -> "OptionGroup":
"""Get (or create) a named option Group.
:param name: Name of the option group.
:param description: Long description for --help output.
:param after: Name of another group, used for ordering --help output.
:returns: The option group.
The returned group object has an ``addoption`` method with the same
signature as :func:`parser.addoption <pytest.Parser.addoption>` but
will be shown in the respective group in the output of
``pytest --help``.
"""
for group in self._groups:
if group.name == name:
return group
group = OptionGroup(name, description, parser=self, _ispytest=True)
i = 0
for i, grp in enumerate(self._groups):
if grp.name == after:
break
self._groups.insert(i + 1, group)
return group
def addoption(self, *opts: str, **attrs: Any) -> None:
"""Register a command line option.
:param opts:
Option names, can be short or long options.
:param attrs:
Same attributes as the argparse library's :py:func:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts.
After command line parsing, options are available on the pytest config
object via ``config.option.NAME`` where ``NAME`` is usually set
by passing a ``dest`` attribute, for example
``addoption("--long", dest="NAME", ...)``.
"""
self._anonymous.addoption(*opts, **attrs)
def parse(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
from _pytest._argcomplete import try_argcomplete
self.optparser = self._getparser()
try_argcomplete(self.optparser)
strargs = [os.fspath(x) for x in args]
return self.optparser.parse_args(strargs, namespace=namespace)
def _getparser(self) -> "MyOptionParser":
from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
groups = self._groups + [self._anonymous]
for group in groups:
if group.options:
desc = group.description or group.name
arggroup = optparser.add_argument_group(desc)
for option in group.options:
n = option.names()
a = option.attrs()
arggroup.add_argument(*n, **a)
file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
# bash like autocompletion for dirs (appending '/')
# Type ignored because typeshed doesn't know about argcomplete.
file_or_dir_arg.completer = filescompleter # type: ignore
return optparser
def parse_setoption(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
option: argparse.Namespace,
namespace: Optional[argparse.Namespace] = None,
) -> List[str]:
parsedoption = self.parse(args, namespace=namespace)
for name, value in parsedoption.__dict__.items():
setattr(option, name, value)
return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
def parse_known_args(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
"""Parse the known arguments at this point.
:returns: An argparse namespace object.
"""
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
def parse_known_and_unknown_args(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> Tuple[argparse.Namespace, List[str]]:
"""Parse the known arguments at this point, and also return the
remaining unknown arguments.
:returns:
A tuple containing an argparse namespace object for the known
arguments, and a list of the unknown arguments.
"""
optparser = self._getparser()
strargs = [os.fspath(x) for x in args]
return optparser.parse_known_args(strargs, namespace=namespace)
def addini(
self,
name: str,
help: str,
type: Optional[
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
] = None,
default: Any = None,
) -> None:
"""Register an ini-file option.
:param name:
Name of the ini-variable.
:param type:
Type of the variable. Can be:
* ``string``: a string
* ``bool``: a boolean
* ``args``: a list of strings, separated as in a shell
* ``linelist``: a list of strings, separated by line breaks
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell
.. versionadded:: 7.0
The ``paths`` variable type.
Defaults to ``string`` if ``None`` or not passed.
:param default:
Default value if no ini-file option exists but is queried.
The value of ini-variables can be retrieved via a call to
:py:func:`config.getini(name) <pytest.Config.getini>`.
"""
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
self._inidict[name] = (help, type, default)
self._ininames.append(name)
class ArgumentError(Exception):
"""Raised if an Argument instance is created with invalid or
inconsistent arguments."""
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
self.msg = msg
self.option_id = str(option)
def __str__(self) -> str:
if self.option_id:
return f"option {self.option_id}: {self.msg}"
else:
return self.msg
class Argument:
"""Class that mimics the necessary behaviour of optparse.Option.
It's currently a least effort implementation and ignoring choices
and integer prefixes.
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
"""
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
def __init__(self, *names: str, **attrs: Any) -> None:
"""Store params in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts: List[str] = []
self._long_opts: List[str] = []
if "%default" in (attrs.get("help") or ""):
warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
try:
typ = attrs["type"]
except KeyError:
pass
else:
# This might raise a keyerror as well, don't want to catch that.
if isinstance(typ, str):
if typ == "choice":
warnings.warn(
ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
stacklevel=4,
)
# argparse expects a type here take it from
# the type of the first element
attrs["type"] = type(attrs["choices"][0])
else:
warnings.warn(
ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
)
attrs["type"] = Argument._typ_map[typ]
# Used in test_parseopt -> test_parse_defaultgetter.
self.type = attrs["type"]
else:
self.type = typ
try:
# Attribute existence is tested in Config._processopt.
self.default = attrs["default"]
except KeyError:
pass
self._set_opt_strings(names)
dest: Optional[str] = attrs.get("dest")
if dest:
self.dest = dest
elif self._long_opts:
self.dest = self._long_opts[0][2:].replace("-", "_")
else:
try:
self.dest = self._short_opts[0][1:]
except IndexError as e:
self.dest = "???" # Needed for the error repr.
raise ArgumentError("need a long or short option", self) from e
def names(self) -> List[str]:
return self._short_opts + self._long_opts
def attrs(self) -> Mapping[str, Any]:
# Update any attributes set by processopt.
attrs = "default dest help".split()
attrs.append(self.dest)
for attr in attrs:
try:
self._attrs[attr] = getattr(self, attr)
except AttributeError:
pass
if self._attrs.get("help"):
a = self._attrs["help"]
a = a.replace("%default", "%(default)s")
# a = a.replace('%prog', '%(prog)s')
self._attrs["help"] = a
return self._attrs
def _set_opt_strings(self, opts: Sequence[str]) -> None:
"""Directly from optparse.
Might not be necessary as this is passed to argparse later on.
"""
for opt in opts:
if len(opt) < 2:
raise ArgumentError(
"invalid option string %r: "
"must be at least two characters long" % opt,
self,
)
elif len(opt) == 2:
if not (opt[0] == "-" and opt[1] != "-"):
raise ArgumentError(
"invalid short option string %r: "
"must be of the form -x, (x any non-dash char)" % opt,
self,
)
self._short_opts.append(opt)
else:
if not (opt[0:2] == "--" and opt[2] != "-"):
raise ArgumentError(
"invalid long option string %r: "
"must start with --, followed by non-dash" % opt,
self,
)
self._long_opts.append(opt)
def __repr__(self) -> str:
args: List[str] = []
if self._short_opts:
args += ["_short_opts: " + repr(self._short_opts)]
if self._long_opts:
args += ["_long_opts: " + repr(self._long_opts)]
args += ["dest: " + repr(self.dest)]
if hasattr(self, "type"):
args += ["type: " + repr(self.type)]
if hasattr(self, "default"):
args += ["default: " + repr(self.default)]
return "Argument({})".format(", ".join(args))
class OptionGroup:
"""A group of options shown in its own section."""
def __init__(
self,
name: str,
description: str = "",
parser: Optional[Parser] = None,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.name = name
self.description = description
self.options: List[Argument] = []
self.parser = parser
def addoption(self, *opts: str, **attrs: Any) -> None:
"""Add an option to this group.
If a shortened version of a long option is specified, it will
be suppressed in the help. ``addoption('--twowords', '--two-words')``
results in help showing ``--two-words`` only, but ``--twowords`` gets
accepted **and** the automatic destination is in ``args.twowords``.
:param opts:
Option names, can be short or long options.
:param attrs:
Same attributes as the argparse library's :py:func:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts.
"""
conflict = set(opts).intersection(
name for opt in self.options for name in opt.names()
)
if conflict:
raise ValueError("option names %s already added" % conflict)
option = Argument(*opts, **attrs)
self._addoption_instance(option, shortupper=False)
def _addoption(self, *opts: str, **attrs: Any) -> None:
option = Argument(*opts, **attrs)
self._addoption_instance(option, shortupper=True)
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
if not shortupper:
for opt in option._short_opts:
if opt[0] == "-" and opt[1].islower():
raise ValueError("lowercase shortoptions reserved")
if self.parser:
self.parser.processoption(option)
self.options.append(option)
class MyOptionParser(argparse.ArgumentParser):
def __init__(
self,
parser: Parser,
extra_info: Optional[Dict[str, Any]] = None,
prog: Optional[str] = None,
) -> None:
self._parser = parser
super().__init__(
prog=prog,
usage=parser._usage,
add_help=False,
formatter_class=DropShorterLongHelpFormatter,
allow_abbrev=False,
)
# extra_info is a dict of (param -> value) to display if there's
# an usage error to provide more contextual information to the user.
self.extra_info = extra_info if extra_info else {}
def error(self, message: str) -> NoReturn:
"""Transform argparse error message into UsageError."""
msg = f"{self.prog}: error: {message}"
if hasattr(self._parser, "_config_source_hint"):
# Type ignored because the attribute is set dynamically.
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
raise UsageError(self.format_usage() + msg)
# Type ignored because typeshed has a very complex type in the superclass.
def parse_args( # type: ignore
self,
args: Optional[Sequence[str]] = None,
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
"""Allow splitting of positional arguments."""
parsed, unrecognized = self.parse_known_args(args, namespace)
if unrecognized:
for arg in unrecognized:
if arg and arg[0] == "-":
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
for k, v in sorted(self.extra_info.items()):
lines.append(f" {k}: {v}")
self.error("\n".join(lines))
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
return parsed
if sys.version_info[:2] < (3, 9): # pragma: no cover
# Backport of https://github.com/python/cpython/pull/14316 so we can
# disable long --argument abbreviations without breaking short flags.
def _parse_optional(
self, arg_string: str
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
if not arg_string:
return None
if not arg_string[0] in self.prefix_chars:
return None
if arg_string in self._option_string_actions:
action = self._option_string_actions[arg_string]
return action, arg_string, None
if len(arg_string) == 1:
return None
if "=" in arg_string:
option_string, explicit_arg = arg_string.split("=", 1)
if option_string in self._option_string_actions:
action = self._option_string_actions[option_string]
return action, option_string, explicit_arg
if self.allow_abbrev or not arg_string.startswith("--"):
option_tuples = self._get_option_tuples(arg_string)
if len(option_tuples) > 1:
msg = gettext(
"ambiguous option: %(option)s could match %(matches)s"
)
options = ", ".join(option for _, option, _ in option_tuples)
self.error(msg % {"option": arg_string, "matches": options})
elif len(option_tuples) == 1:
(option_tuple,) = option_tuples
return option_tuple
if self._negative_number_matcher.match(arg_string):
if not self._has_negative_number_optionals:
return None
if " " in arg_string:
return None
return None, arg_string, None
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
"""Shorten help for long options that differ only in extra hyphens.
- Collapse **long** options that are the same except for extra hyphens.
- Shortcut if there are only two options and one of them is a short one.
- Cache result on the action object as this is called at least 2 times.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
# Use more accurate terminal width.
if "width" not in kwargs:
kwargs["width"] = _pytest._io.get_terminal_width()
super().__init__(*args, **kwargs)
def _format_action_invocation(self, action: argparse.Action) -> str:
orgstr = super()._format_action_invocation(action)
if orgstr and orgstr[0] != "-": # only optional arguments
return orgstr
res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
if res:
return res
options = orgstr.split(", ")
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
# a shortcut for '-h, --help' or '--abc', '-a'
action._formatted_action_invocation = orgstr # type: ignore
return orgstr
return_list = []
short_long: Dict[str, str] = {}
for option in options:
if len(option) == 2 or option[2] == " ":
continue
if not option.startswith("--"):
raise ArgumentError(
'long optional argument without "--": [%s]' % (option), option
)
xxoption = option[2:]
shortened = xxoption.replace("-", "")
if shortened not in short_long or len(short_long[shortened]) < len(
xxoption
):
short_long[shortened] = xxoption
# now short_long has been filled out to the longest with dashes
# **and** we keep the right option ordering from add_argument
for option in options:
if len(option) == 2 or option[2] == " ":
return_list.append(option)
if option[2:] == short_long.get(option.replace("-", "")):
return_list.append(option.replace(" ", "=", 1))
formatted_action_invocation = ", ".join(return_list)
action._formatted_action_invocation = formatted_action_invocation # type: ignore
return formatted_action_invocation
def _split_lines(self, text, width):
"""Wrap lines after splitting on original newlines.
This allows to have explicit line breaks in the help text.
"""
import textwrap
lines = []
for line in text.splitlines():
lines.extend(textwrap.wrap(line.strip(), width))
return lines

View File

@ -1,71 +0,0 @@
import functools
import warnings
from pathlib import Path
from typing import Optional
from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH)
imply_paths_hooks = {
"pytest_ignore_collect": ("collection_path", "path"),
"pytest_collect_file": ("file_path", "path"),
"pytest_pycollect_makemodule": ("module_path", "path"),
"pytest_report_header": ("start_path", "startdir"),
"pytest_report_collectionfinish": ("start_path", "startdir"),
}
class PathAwareHookProxy:
"""
this helper wraps around hook callers
until pluggy supports fixingcalls, this one will do
it currently doesn't return full hook caller proxies for fixed hooks,
this may have to be changed later depending on bugs
"""
def __init__(self, hook_caller):
self.__hook_caller = hook_caller
def __dir__(self):
return dir(self.__hook_caller)
def __getattr__(self, key, _wraps=functools.wraps):
hook = getattr(self.__hook_caller, key)
if key not in imply_paths_hooks:
self.__dict__[key] = hook
return hook
else:
path_var, fspath_var = imply_paths_hooks[key]
@_wraps(hook)
def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None:
warnings.warn(
HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
),
stacklevel=2,
)
if path_value is not None:
if fspath_value is not None:
_check_path(path_value, fspath_value)
else:
fspath_value = legacy_path(path_value)
else:
assert fspath_value is not None
path_value = Path(fspath_value)
kw[path_var] = path_value
kw[fspath_var] = fspath_value
return hook(**kw)
fixed_hook.__name__ = key
self.__dict__[key] = fixed_hook
return fixed_hook

View File

@ -1,11 +0,0 @@
from _pytest.compat import final
@final
class UsageError(Exception):
"""Error in pytest usage or invocation."""
class PrintHelp(Exception):
"""Raised when pytest should print its help to skip the rest of the
argument parsing and validation."""

View File

@ -1,218 +0,0 @@
import os
import sys
from pathlib import Path
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import iniconfig
from .exceptions import UsageError
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
if TYPE_CHECKING:
from . import Config
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
the parsed object.
Raise UsageError if the file cannot be parsed.
"""
try:
return iniconfig.IniConfig(str(path))
except iniconfig.ParseError as exc:
raise UsageError(str(exc)) from exc
def load_config_dict_from_file(
filepath: Path,
) -> Optional[Dict[str, Union[str, List[str]]]]:
"""Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration.
"""
# Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath)
if "pytest" in iniconfig:
return dict(iniconfig["pytest"].items())
else:
# "pytest.ini" files are always the source of configuration, even if empty.
if filepath.name == "pytest.ini":
return {}
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
elif filepath.suffix == ".cfg":
iniconfig = _parse_ini_config(filepath)
if "tool:pytest" in iniconfig.sections:
return dict(iniconfig["tool:pytest"].items())
elif "pytest" in iniconfig.sections:
# If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
elif filepath.suffix == ".toml":
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
toml_text = filepath.read_text(encoding="utf-8")
try:
config = tomllib.loads(toml_text)
except tomllib.TOMLDecodeError as exc:
raise UsageError(f"{filepath}: {exc}") from exc
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
if result is not None:
# TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
# however we need to convert all scalar values to str for compatibility with the rest
# of the configuration system, which expects strings only.
def make_scalar(v: object) -> Union[str, List[str]]:
return v if isinstance(v, list) else str(v)
return {k: make_scalar(v) for k, v in result.items()}
return None
def locate_config(
args: Iterable[Path],
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Search in the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
"pytest.ini",
".pytest.ini",
"pyproject.toml",
"tox.ini",
"setup.cfg",
]
args = [x for x in args if not str(x).startswith("-")]
if not args:
args = [Path.cwd()]
for arg in args:
argpath = absolutepath(arg)
for base in (argpath, *argpath.parents):
for config_name in config_names:
p = base / config_name
if p.is_file():
ini_config = load_config_dict_from_file(p)
if ini_config is not None:
return base, p, ini_config
return None, None, {}
def get_common_ancestor(paths: Iterable[Path]) -> Path:
common_ancestor: Optional[Path] = None
for path in paths:
if not path.exists():
continue
if common_ancestor is None:
common_ancestor = path
else:
if common_ancestor in path.parents or path == common_ancestor:
continue
elif path in common_ancestor.parents:
common_ancestor = path
else:
shared = commonpath(path, common_ancestor)
if shared is not None:
common_ancestor = shared
if common_ancestor is None:
common_ancestor = Path.cwd()
elif common_ancestor.is_file():
common_ancestor = common_ancestor.parent
return common_ancestor
def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
def is_option(x: str) -> bool:
return x.startswith("-")
def get_file_part_from_node_id(x: str) -> str:
return x.split("::")[0]
def get_dir_from_path(path: Path) -> Path:
if path.is_dir():
return path
return path.parent
def safe_exists(path: Path) -> bool:
# This can throw on paths that contain characters unrepresentable at the OS level,
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
try:
return path.exists()
except OSError:
return False
# These look like paths but may not exist
possible_paths = (
absolutepath(get_file_part_from_node_id(arg))
for arg in args
if not is_option(arg)
)
return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
def determine_setup(
inifile: Optional[str],
args: Sequence[str],
rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None,
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
rootdir = None
dirs = get_dirs_from_args(args)
if inifile:
inipath_ = absolutepath(inifile)
inipath: Optional[Path] = inipath_
inicfg = load_config_dict_from_file(inipath_) or {}
if rootdir_cmd_arg is None:
rootdir = inipath_.parent
else:
ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor])
if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in (ancestor, *ancestor.parents):
if (possible_rootdir / "setup.py").is_file():
rootdir = possible_rootdir
break
else:
if dirs != [ancestor]:
rootdir, inipath, inicfg = locate_config(dirs)
if rootdir is None:
if config is not None:
cwd = config.invocation_params.dir
else:
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
if is_fs_root:
rootdir = ancestor
if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
if not rootdir.is_dir():
raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format(
rootdir
)
)
assert rootdir is not None
return rootdir, inipath, inicfg or {}

View File

@ -1,391 +0,0 @@
"""Interactive debugging with PDB, the Python Debugger."""
import argparse
import functools
import sys
import types
import unittest
from typing import Any
from typing import Callable
from typing import Generator
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
from _pytest import outcomes
from _pytest._code import ExceptionInfo
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager
from _pytest.config.argparsing import Parser
from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node
from _pytest.reports import BaseReport
if TYPE_CHECKING:
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
"""Validate syntax of --pdbcls option."""
try:
modname, classname = value.split(":")
except ValueError as e:
raise argparse.ArgumentTypeError(
f"{value!r} is not in the format 'modname:classname'"
) from e
return (modname, classname)
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group._addoption(
"--pdb",
dest="usepdb",
action="store_true",
help="Start the interactive Python debugger on errors or KeyboardInterrupt",
)
group._addoption(
"--pdbcls",
dest="usepdb_cls",
metavar="modulename:classname",
type=_validate_usepdb_cls,
help="Specify a custom interactive Python debugger for use with --pdb."
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
)
group._addoption(
"--trace",
dest="trace",
action="store_true",
help="Immediately break when running each test",
)
def pytest_configure(config: Config) -> None:
import pdb
if config.getvalue("trace"):
config.pluginmanager.register(PdbTrace(), "pdbtrace")
if config.getvalue("usepdb"):
config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
pytestPDB._saved.append(
(pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
)
pdb.set_trace = pytestPDB.set_trace
pytestPDB._pluginmanager = config.pluginmanager
pytestPDB._config = config
# NOTE: not using pytest_unconfigure, since it might get called although
# pytest_configure was not (if another plugin raises UsageError).
def fin() -> None:
(
pdb.set_trace,
pytestPDB._pluginmanager,
pytestPDB._config,
) = pytestPDB._saved.pop()
config.add_cleanup(fin)
class pytestPDB:
"""Pseudo PDB that defers to the real pdb."""
_pluginmanager: Optional[PytestPluginManager] = None
_config: Optional[Config] = None
_saved: List[
Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
] = []
_recursive_debug = 0
_wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
@classmethod
def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
if capman:
return capman.is_capturing()
return False
@classmethod
def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
if not cls._config:
import pdb
# Happens when using pytest.set_trace outside of a test.
return pdb.Pdb
usepdb_cls = cls._config.getvalue("usepdb_cls")
if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
return cls._wrapped_pdb_cls[1]
if usepdb_cls:
modname, classname = usepdb_cls
try:
__import__(modname)
mod = sys.modules[modname]
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
parts = classname.split(".")
pdb_cls = getattr(mod, parts[0])
for part in parts[1:]:
pdb_cls = getattr(pdb_cls, part)
except Exception as exc:
value = ":".join((modname, classname))
raise UsageError(
f"--pdbcls: could not import {value!r}: {exc}"
) from exc
else:
import pdb
pdb_cls = pdb.Pdb
wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
return wrapped_cls
@classmethod
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
import _pytest.config
# Type ignored because mypy doesn't support "dynamic"
# inheritance like this.
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
_pytest_capman = capman
_continued = False
def do_debug(self, arg):
cls._recursive_debug += 1
ret = super().do_debug(arg)
cls._recursive_debug -= 1
return ret
def do_continue(self, arg):
ret = super().do_continue(arg)
if cls._recursive_debug == 0:
assert cls._config is not None
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
capman = self._pytest_capman
capturing = pytestPDB._is_capturing(capman)
if capturing:
if capturing == "global":
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(
">",
"PDB continue (IO-capturing resumed for %s)"
% capturing,
)
assert capman is not None
capman.resume()
else:
tw.sep(">", "PDB continue")
assert cls._pluginmanager is not None
cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
self._continued = True
return ret
do_c = do_cont = do_continue
def do_quit(self, arg):
"""Raise Exit outcome when quit command is used in pdb.
This is a bit of a hack - it would be better if BdbQuit
could be handled, but this would require to wrap the
whole pytest run, and adjust the report etc.
"""
ret = super().do_quit(arg)
if cls._recursive_debug == 0:
outcomes.exit("Quitting debugger")
return ret
do_q = do_quit
do_exit = do_quit
def setup(self, f, tb):
"""Suspend on setup().
Needed after do_continue resumed, and entering another
breakpoint again.
"""
ret = super().setup(f, tb)
if not ret and self._continued:
# pdb.setup() returns True if the command wants to exit
# from the interaction: do not suspend capturing then.
if self._pytest_capman:
self._pytest_capman.suspend_global_capture(in_=True)
return ret
def get_stack(self, f, t):
stack, i = super().get_stack(f, t)
if f is None:
# Find last non-hidden frame.
i = max(0, len(stack) - 1)
while i and stack[i][0].f_locals.get("__tracebackhide__", False):
i -= 1
return stack, i
return PytestPdbWrapper
@classmethod
def _init_pdb(cls, method, *args, **kwargs):
"""Initialize PDB debugging, dropping any IO capturing."""
import _pytest.config
if cls._pluginmanager is None:
capman: Optional[CaptureManager] = None
else:
capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend(in_=True)
if cls._config:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if cls._recursive_debug == 0:
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
else:
capturing = cls._is_capturing(capman)
if capturing == "global":
tw.sep(">", f"PDB {method} (IO-capturing turned off)")
elif capturing:
tw.sep(
">",
"PDB %s (IO-capturing turned off for %s)"
% (method, capturing),
)
else:
tw.sep(">", f"PDB {method}")
_pdb = cls._import_pdb_cls(capman)(**kwargs)
if cls._pluginmanager:
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
return _pdb
@classmethod
def set_trace(cls, *args, **kwargs) -> None:
"""Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
frame = sys._getframe().f_back
_pdb = cls._init_pdb("set_trace", *args, **kwargs)
_pdb.set_trace(frame)
class PdbInvoke:
def pytest_exception_interact(
self, node: Node, call: "CallInfo[Any]", report: BaseReport
) -> None:
capman = node.config.pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture(in_=True)
out, err = capman.read_global_capture()
sys.stdout.write(out)
sys.stdout.write(err)
assert call.excinfo is not None
if not isinstance(call.excinfo.value, unittest.SkipTest):
_enter_pdb(node, call.excinfo, report)
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
tb = _postmortem_traceback(excinfo)
post_mortem(tb)
class PdbTrace:
@hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
wrap_pytest_function_for_tracing(pyfuncitem)
yield
def wrap_pytest_function_for_tracing(pyfuncitem):
"""Change the Python function object of the given Function item by a
wrapper which actually enters pdb before calling the python function
itself, effectively leaving the user in the pdb prompt in the first
statement of the function."""
_pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj
# we can't just return `partial(pdb.runcall, testfunction)` because (on
# python < 3.7.4) runcall's first param is `func`, which means we'd get
# an exception if one of the kwargs to testfunction was called `func`.
@functools.wraps(testfunction)
def wrapper(*args, **kwargs):
func = functools.partial(testfunction, *args, **kwargs)
_pdb.runcall(func)
pyfuncitem.obj = wrapper
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
"""Wrap the given pytestfunct item for tracing support if --trace was given in
the command line."""
if pyfuncitem.config.getvalue("trace"):
wrap_pytest_function_for_tracing(pyfuncitem)
def _enter_pdb(
node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
) -> BaseReport:
# XXX we re-use the TerminalReporter's terminalwriter
# because this seems to avoid some encoding related troubles
# for not completely clear reasons.
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
tw.line()
showcapture = node.config.option.showcapture
for sectionname, content in (
("stdout", rep.capstdout),
("stderr", rep.capstderr),
("log", rep.caplog),
):
if showcapture in (sectionname, "all") and content:
tw.sep(">", "captured " + sectionname)
if content[-1:] == "\n":
content = content[:-1]
tw.line(content)
tw.sep(">", "traceback")
rep.toterminal(tw)
tw.sep(">", "entering PDB")
tb = _postmortem_traceback(excinfo)
rep._pdbshown = True # type: ignore[attr-defined]
post_mortem(tb)
return rep
def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
from doctest import UnexpectedException
if isinstance(excinfo.value, UnexpectedException):
# A doctest.UnexpectedException is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.exc_info[2]
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.excinfo[2]
else:
assert excinfo._excinfo is not None
return excinfo._excinfo[2]
def post_mortem(t: types.TracebackType) -> None:
p = pytestPDB._init_pdb("post_mortem")
p.reset()
p.interaction(None, t)
if p.quitting:
outcomes.exit("Quitting debugger")

View File

@ -1,146 +0,0 @@
"""Deprecation messages and bits of code used elsewhere in the codebase that
is planned to be removed in the next pytest release.
Keeping it in a central location makes it easy to track what is deprecated and should
be removed when the time comes.
All constants defined in this module should be either instances of
:class:`PytestWarning`, or :class:`UnformattedWarning`
in case of warnings which need to format their messages.
"""
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import UnformattedWarning
# set of plugins which have been integrated into the core; we use this list to ignore
# them during registration to avoid conflicts
DEPRECATED_EXTERNAL_PLUGINS = {
"pytest_catchlog",
"pytest_capturelog",
"pytest_faulthandler",
}
NOSE_SUPPORT = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose method: `{method}` ({stage})\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
NOSE_SUPPORT_METHOD = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose-specific method: `{method}(self)`\n"
"To remove this warning, rename it to `{method}_method(self)`\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
# * If you're in the future: "could have been".
YIELD_FIXTURE = PytestDeprecationWarning(
"@pytest.yield_fixture is deprecated.\n"
"Use @pytest.fixture instead; they are the same."
)
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
"Please use pytest_load_initial_conftests hook instead."
)
FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning(
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
)
STRICT_OPTION = PytestRemovedIn8Warning(
"The --strict option is deprecated, use --strict-markers instead."
)
# This deprecation is never really meant to be removed.
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
)
ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning(
PytestRemovedIn8Warning,
"`type` argument to addoption() is the string {typ!r}."
" For choices this is optional and can be omitted, "
" but when supplied should be a type (for example `str` or `int`)."
" (options: {names})",
)
ARGUMENT_TYPE_STR = UnformattedWarning(
PytestRemovedIn8Warning,
"`type` argument to addoption() is the string {typ!r}, "
" but when supplied should be a type (for example `str` or `int`)."
" (options: {names})",
)
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
PytestRemovedIn8Warning,
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
"see https://docs.pytest.org/en/latest/deprecations.html"
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
)
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
PytestRemovedIn8Warning,
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
"Please use the (path: pathlib.Path) argument instead.\n"
"See https://docs.pytest.org/en/latest/deprecations.html"
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
)
WARNS_NONE_ARG = PytestRemovedIn8Warning(
"Passing None has been deprecated.\n"
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
"#additional-use-cases-of-warnings-in-tests"
" for alternatives in common use cases."
)
KEYWORD_MSG_ARG = UnformattedWarning(
PytestRemovedIn8Warning,
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
)
INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
"The pytest.Instance collector type is deprecated and is no longer used. "
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
)
HOOK_LEGACY_MARKING = UnformattedWarning(
PytestDeprecationWarning,
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
"Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
" to configure the hooks.\n"
" See https://docs.pytest.org/en/latest/deprecations.html"
"#configuring-hook-specs-impls-using-markers",
)
# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
# ...
#
# Do this:
#
# def my_private_function(some, args, *, _ispytest: bool = False):
# check_ispytest(_ispytest)
# ...
#
# Change all internal/allowed calls to
#
# my_private_function(some, args, _ispytest=True)
#
# All other calls will get the default _ispytest=False and trigger
# the warning (possibly error in the future).
def check_ispytest(ispytest: bool) -> None:
if not ispytest:
warn(PRIVATE, stacklevel=3)

View File

@ -1,752 +0,0 @@
"""Discover and run doctests in modules and test files."""
import bdb
import inspect
import os
import platform
import sys
import traceback
import types
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python import Module
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
DOCTEST_REPORT_CHOICES = (
DOCTEST_REPORT_CHOICE_NONE,
DOCTEST_REPORT_CHOICE_CDIFF,
DOCTEST_REPORT_CHOICE_NDIFF,
DOCTEST_REPORT_CHOICE_UDIFF,
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
)
# Lazy definition of runner class
RUNNER_CLASS = None
# Lazy definition of output checker class
CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"doctest_optionflags",
"Option flags for doctests",
type="args",
default=["ELLIPSIS"],
)
parser.addini(
"doctest_encoding", "Encoding used for doctest files", default="utf-8"
)
group = parser.getgroup("collect")
group.addoption(
"--doctest-modules",
action="store_true",
default=False,
help="Run doctests in all .py modules",
dest="doctestmodules",
)
group.addoption(
"--doctest-report",
type=str.lower,
default="udiff",
help="Choose another output format for diffs on doctest failure",
choices=DOCTEST_REPORT_CHOICES,
dest="doctestreport",
)
group.addoption(
"--doctest-glob",
action="append",
default=[],
metavar="pat",
help="Doctests file matching pattern, default: test*.txt",
dest="doctestglob",
)
group.addoption(
"--doctest-ignore-import-errors",
action="store_true",
default=False,
help="Ignore doctest ImportErrors",
dest="doctest_ignore_import_errors",
)
group.addoption(
"--doctest-continue-on-failure",
action="store_true",
default=False,
help="For a given doctest, continue to run after the first failure",
dest="doctest_continue_on_failure",
)
def pytest_unconfigure() -> None:
global RUNNER_CLASS
RUNNER_CLASS = None
def pytest_collect_file(
file_path: Path,
parent: Collector,
) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
config = parent.config
if file_path.suffix == ".py":
if config.option.doctestmodules and not any(
(_is_setup_py(file_path), _is_main_py(file_path))
):
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
return mod
elif _is_doctest(config, file_path, parent):
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
return txt
return None
def _is_setup_py(path: Path) -> bool:
if path.name != "setup.py":
return False
contents = path.read_bytes()
return b"setuptools" in contents or b"distutils" in contents
def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
return True
globs = config.getoption("doctestglob") or ["test*.txt"]
return any(fnmatch_ex(glob, path) for glob in globs)
def _is_main_py(path: Path) -> bool:
return path.name == "__main__.py"
class ReprFailDoctest(TerminalRepr):
def __init__(
self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
) -> None:
self.reprlocation_lines = reprlocation_lines
def toterminal(self, tw: TerminalWriter) -> None:
for reprlocation, lines in self.reprlocation_lines:
for line in lines:
tw.line(line)
reprlocation.toterminal(tw)
class MultipleDoctestFailures(Exception):
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
super().__init__()
self.failures = failures
def _init_runner_class() -> Type["doctest.DocTestRunner"]:
import doctest
class PytestDoctestRunner(doctest.DebugRunner):
"""Runner to collect failures.
Note that the out variable in this case is a list instead of a
stdout-like object.
"""
def __init__(
self,
checker: Optional["doctest.OutputChecker"] = None,
verbose: Optional[bool] = None,
optionflags: int = 0,
continue_on_failure: bool = True,
) -> None:
super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
self.continue_on_failure = continue_on_failure
def report_failure(
self,
out,
test: "doctest.DocTest",
example: "doctest.Example",
got: str,
) -> None:
failure = doctest.DocTestFailure(test, example, got)
if self.continue_on_failure:
out.append(failure)
else:
raise failure
def report_unexpected_exception(
self,
out,
test: "doctest.DocTest",
example: "doctest.Example",
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
) -> None:
if isinstance(exc_info[1], OutcomeException):
raise exc_info[1]
if isinstance(exc_info[1], bdb.BdbQuit):
outcomes.exit("Quitting debugger")
failure = doctest.UnexpectedException(test, example, exc_info)
if self.continue_on_failure:
out.append(failure)
else:
raise failure
return PytestDoctestRunner
def _get_runner(
checker: Optional["doctest.OutputChecker"] = None,
verbose: Optional[bool] = None,
optionflags: int = 0,
continue_on_failure: bool = True,
) -> "doctest.DocTestRunner":
# We need this in order to do a lazy import on doctest
global RUNNER_CLASS
if RUNNER_CLASS is None:
RUNNER_CLASS = _init_runner_class()
# Type ignored because the continue_on_failure argument is only defined on
# PytestDoctestRunner, which is lazily defined so can't be used as a type.
return RUNNER_CLASS( # type: ignore
checker=checker,
verbose=verbose,
optionflags=optionflags,
continue_on_failure=continue_on_failure,
)
class DoctestItem(Item):
def __init__(
self,
name: str,
parent: "Union[DoctestTextfile, DoctestModule]",
runner: Optional["doctest.DocTestRunner"] = None,
dtest: Optional["doctest.DocTest"] = None,
) -> None:
super().__init__(name, parent)
self.runner = runner
self.dtest = dtest
self.obj = None
self.fixture_request: Optional[FixtureRequest] = None
@classmethod
def from_parent( # type: ignore
cls,
parent: "Union[DoctestTextfile, DoctestModule]",
*,
name: str,
runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest",
):
# incompatible signature due to imposed limits on subclass
"""The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
def setup(self) -> None:
if self.dtest is not None:
self.fixture_request = _setup_fixtures(self)
globs = dict(getfixture=self.fixture_request.getfixturevalue)
for name, value in self.fixture_request.getfixturevalue(
"doctest_namespace"
).items():
globs[name] = value
self.dtest.globs.update(globs)
def runtest(self) -> None:
assert self.dtest is not None
assert self.runner is not None
_check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin()
failures: List["doctest.DocTestFailure"] = []
# Type ignored because we change the type of `out` from what
# doctest expects.
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
if failures:
raise MultipleDoctestFailures(failures)
def _disable_output_capturing_for_darwin(self) -> None:
"""Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
if platform.system() != "Darwin":
return
capman = self.config.pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture(in_=True)
out, err = capman.read_global_capture()
sys.stdout.write(out)
sys.stderr.write(err)
# TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
self,
excinfo: ExceptionInfo[BaseException],
) -> Union[str, TerminalRepr]:
import doctest
failures: Optional[
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
] = None
if isinstance(
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
):
failures = [excinfo.value]
elif isinstance(excinfo.value, MultipleDoctestFailures):
failures = excinfo.value.failures
if failures is None:
return super().repr_failure(excinfo)
reprlocation_lines = []
for failure in failures:
example = failure.example
test = failure.test
filename = test.filename
if test.lineno is None:
lineno = None
else:
lineno = test.lineno + example.lineno + 1
message = type(failure).__name__
# TODO: ReprFileLocation doesn't expect a None lineno.
reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
checker = _get_checker()
report_choice = _get_report_choice(self.config.getoption("doctestreport"))
if lineno is not None:
assert failure.test.docstring is not None
lines = failure.test.docstring.splitlines(False)
# add line numbers to the left of the error message
assert test.lineno is not None
lines = [
"%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
]
# trim docstring error lines to 10
lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
else:
lines = [
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
]
indent = ">>>"
for line in example.source.splitlines():
lines.append(f"??? {indent} {line}")
indent = "..."
if isinstance(failure, doctest.DocTestFailure):
lines += checker.output_difference(
example, failure.got, report_choice
).split("\n")
else:
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
lines += [
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
]
reprlocation_lines.append((reprlocation, lines))
return ReprFailDoctest(reprlocation_lines)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
assert self.dtest is not None
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
def _get_flag_lookup() -> Dict[str, int]:
import doctest
return dict(
DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
ELLIPSIS=doctest.ELLIPSIS,
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
ALLOW_UNICODE=_get_allow_unicode_flag(),
ALLOW_BYTES=_get_allow_bytes_flag(),
NUMBER=_get_number_flag(),
)
def get_optionflags(parent):
optionflags_str = parent.config.getini("doctest_optionflags")
flag_lookup_table = _get_flag_lookup()
flag_acc = 0
for flag in optionflags_str:
flag_acc |= flag_lookup_table[flag]
return flag_acc
def _get_continue_on_failure(config):
continue_on_failure = config.getvalue("doctest_continue_on_failure")
if continue_on_failure:
# We need to turn off this if we use pdb since we should stop at
# the first failure.
if config.getvalue("usepdb"):
continue_on_failure = False
return continue_on_failure
class DoctestTextfile(Module):
obj = None
def collect(self) -> Iterable[DoctestItem]:
import doctest
# Inspired by doctest.testfile; ideally we would use it directly,
# but it doesn't support passing a custom checker.
encoding = self.config.getini("doctest_encoding")
text = self.path.read_text(encoding)
filename = str(self.path)
name = self.path.name
globs = {"__name__": "__main__"}
optionflags = get_optionflags(self)
runner = _get_runner(
verbose=False,
optionflags=optionflags,
checker=_get_checker(),
continue_on_failure=_get_continue_on_failure(self.config),
)
parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0)
if test.examples:
yield DoctestItem.from_parent(
self, name=test.name, runner=runner, dtest=test
)
def _check_all_skipped(test: "doctest.DocTest") -> None:
"""Raise pytest.skip() if all examples in the given DocTest have the SKIP
option set."""
import doctest
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
if all_skipped:
skip("all tests skipped by +SKIP option")
def _is_mocked(obj: object) -> bool:
"""Return if an object is possibly a mock object by checking the
existence of a highly improbable attribute."""
return (
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
is not None
)
@contextmanager
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
"""Context manager which replaces ``inspect.unwrap`` with a version
that's aware of mock objects and doesn't recurse into them."""
real_unwrap = inspect.unwrap
def _mock_aware_unwrap(
func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
) -> Any:
try:
if stop is None or stop is _is_mocked:
return real_unwrap(func, stop=_is_mocked)
_stop = stop
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
except Exception as e:
warnings.warn(
"Got %r when unwrapping %r. This is usually caused "
"by a violation of Python's object protocol; see e.g. "
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
PytestWarning,
)
raise
inspect.unwrap = _mock_aware_unwrap
try:
yield
finally:
inspect.unwrap = real_unwrap
class DoctestModule(Module):
def collect(self) -> Iterable[DoctestItem]:
import doctest
class MockAwareDocTestFinder(doctest.DocTestFinder):
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
https://github.com/pytest-dev/pytest/issues/3456
https://bugs.python.org/issue25532
"""
def _find_lineno(self, obj, source_lines):
"""Doctest code does not take into account `@property`, this
is a hackish way to fix it. https://bugs.python.org/issue17446
Wrapped Doctests will need to be unwrapped so the correct
line number is returned. This will be reported upstream. #8796
"""
if isinstance(obj, property):
obj = getattr(obj, "fget", obj)
if hasattr(obj, "__wrapped__"):
# Get the main obj in case of it being wrapped
obj = inspect.unwrap(obj)
# Type ignored because this is a private function.
return super()._find_lineno( # type:ignore[misc]
obj,
source_lines,
)
def _find(
self, tests, obj, name, module, source_lines, globs, seen
) -> None:
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
)
if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path,
self.config.getoption("importmode"),
rootpath=self.config.rootpath,
)
else:
try:
module = import_path(
self.path,
root=self.config.rootpath,
mode=self.config.getoption("importmode"),
)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
# Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self)
runner = _get_runner(
verbose=False,
optionflags=optionflags,
checker=_get_checker(),
continue_on_failure=_get_continue_on_failure(self.config),
)
for test in finder.find(module, module.__name__):
if test.examples: # skip empty doctests
yield DoctestItem.from_parent(
self, name=test.name, runner=runner, dtest=test
)
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
def func() -> None:
pass
doctest_item.funcargs = {} # type: ignore[attr-defined]
fm = doctest_item.session._fixturemanager
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
fixture_request._fillfixtures()
return fixture_request
def _init_checker_class() -> Type["doctest.OutputChecker"]:
import doctest
import re
class LiteralsOutputChecker(doctest.OutputChecker):
# Based on doctest_nose_plugin.py from the nltk project
# (https://github.com/nltk/nltk) and on the "numtest" doctest extension
# by Sebastien Boisgerault (https://github.com/boisgera/numtest).
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
_number_re = re.compile(
r"""
(?P<number>
(?P<mantissa>
(?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
|
(?P<integer2> [+-]?\d+)\.
)
(?:
[Ee]
(?P<exponent1> [+-]?\d+)
)?
|
(?P<integer3> [+-]?\d+)
(?:
[Ee]
(?P<exponent2> [+-]?\d+)
)
)
""",
re.VERBOSE,
)
def check_output(self, want: str, got: str, optionflags: int) -> bool:
if super().check_output(want, got, optionflags):
return True
allow_unicode = optionflags & _get_allow_unicode_flag()
allow_bytes = optionflags & _get_allow_bytes_flag()
allow_number = optionflags & _get_number_flag()
if not allow_unicode and not allow_bytes and not allow_number:
return False
def remove_prefixes(regex: Pattern[str], txt: str) -> str:
return re.sub(regex, r"\1\2", txt)
if allow_unicode:
want = remove_prefixes(self._unicode_literal_re, want)
got = remove_prefixes(self._unicode_literal_re, got)
if allow_bytes:
want = remove_prefixes(self._bytes_literal_re, want)
got = remove_prefixes(self._bytes_literal_re, got)
if allow_number:
got = self._remove_unwanted_precision(want, got)
return super().check_output(want, got, optionflags)
def _remove_unwanted_precision(self, want: str, got: str) -> str:
wants = list(self._number_re.finditer(want))
gots = list(self._number_re.finditer(got))
if len(wants) != len(gots):
return got
offset = 0
for w, g in zip(wants, gots):
fraction: Optional[str] = w.group("fraction")
exponent: Optional[str] = w.group("exponent1")
if exponent is None:
exponent = w.group("exponent2")
precision = 0 if fraction is None else len(fraction)
if exponent is not None:
precision -= int(exponent)
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
# They're close enough. Replace the text we actually
# got with the text we want, so that it will match when we
# check the string literally.
got = (
got[: g.start() + offset] + w.group() + got[g.end() + offset :]
)
offset += w.end() - w.start() - (g.end() - g.start())
return got
return LiteralsOutputChecker
def _get_checker() -> "doctest.OutputChecker":
"""Return a doctest.OutputChecker subclass that supports some
additional options:
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
prefixes (respectively) in string literals. Useful when the same
doctest should run in Python 2 and Python 3.
* NUMBER to ignore floating-point differences smaller than the
precision of the literal number in the doctest.
An inner class is used to avoid importing "doctest" at the module
level.
"""
global CHECKER_CLASS
if CHECKER_CLASS is None:
CHECKER_CLASS = _init_checker_class()
return CHECKER_CLASS()
def _get_allow_unicode_flag() -> int:
"""Register and return the ALLOW_UNICODE flag."""
import doctest
return doctest.register_optionflag("ALLOW_UNICODE")
def _get_allow_bytes_flag() -> int:
"""Register and return the ALLOW_BYTES flag."""
import doctest
return doctest.register_optionflag("ALLOW_BYTES")
def _get_number_flag() -> int:
"""Register and return the NUMBER flag."""
import doctest
return doctest.register_optionflag("NUMBER")
def _get_report_choice(key: str) -> int:
"""Return the actual `doctest` module flag value.
We want to do it as late as possible to avoid importing `doctest` and all
its dependencies when parsing options, as it adds overhead and breaks tests.
"""
import doctest
return {
DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
DOCTEST_REPORT_CHOICE_NONE: 0,
}[key]
@fixture(scope="session")
def doctest_namespace() -> Dict[str, Any]:
"""Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
Usually this fixture is used in conjunction with another ``autouse`` fixture:
.. code-block:: python
@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
doctest_namespace["np"] = numpy
For more details: :ref:`doctest_namespace`.
"""
return dict()

View File

@ -1,97 +0,0 @@
import io
import os
import sys
from typing import Generator
from typing import TextIO
import pytest
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
from _pytest.stash import StashKey
fault_handler_stderr_key = StashKey[TextIO]()
fault_handler_originally_enabled_key = StashKey[bool]()
def pytest_addoption(parser: Parser) -> None:
help = (
"Dump the traceback of all threads if a test takes "
"more than TIMEOUT seconds to finish"
)
parser.addini("faulthandler_timeout", help, default=0.0)
def pytest_configure(config: Config) -> None:
import faulthandler
stderr_fd_copy = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
def pytest_unconfigure(config: Config) -> None:
import faulthandler
faulthandler.disable()
# Close the dup file installed during pytest_configure.
if fault_handler_stderr_key in config.stash:
config.stash[fault_handler_stderr_key].close()
del config.stash[fault_handler_stderr_key]
if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno())
def get_stderr_fileno() -> int:
try:
fileno = sys.stderr.fileno()
# The Twisted Logger will return an invalid file descriptor since it is not backed
# by an FD. So, let's also forward this to the same code path as with pytest-xdist.
if fileno == -1:
raise AttributeError()
return fileno
except (AttributeError, io.UnsupportedOperation):
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
# This is potentially dangerous, but the best we can do.
return sys.__stderr__.fileno()
def get_timeout_config_value(config: Config) -> float:
return float(config.getini("faulthandler_timeout") or 0.0)
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config)
stderr = item.config.stash[fault_handler_stderr_key]
if timeout > 0 and stderr is not None:
import faulthandler
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield
finally:
faulthandler.cancel_dump_traceback_later()
else:
yield
@pytest.hookimpl(tryfirst=True)
def pytest_enter_pdb() -> None:
"""Cancel any traceback dumping due to timeout before entering pdb."""
import faulthandler
faulthandler.cancel_dump_traceback_later()
@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact() -> None:
"""Cancel any traceback dumping due to an interactive exception being
raised."""
import faulthandler
faulthandler.cancel_dump_traceback_later()

Some files were not shown because too many files have changed in this diff Show More