Compare commits
19 Commits
268e3b6a47
...
master
Author | SHA1 | Date | |
---|---|---|---|
4ecaa1a216 | |||
fcef1277f3 | |||
8507082995 | |||
e091a825ce | |||
8501f2f473 | |||
bfdc5dc93b | |||
d81425a781 | |||
62f9f2677d | |||
95918841b0 | |||
419e67f80a | |||
38135762f6 | |||
5c03647a4a | |||
3ee1155e1f | |||
efb36f83fc | |||
d28bd6ed1c | |||
45c5b94e63 | |||
0f1e8d5e36 | |||
57405f5aac | |||
8500f351fc |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -3,3 +3,4 @@
|
|||||||
|
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
|
*.exe binary
|
178
assignments/A4/life.js
Normal file
178
assignments/A4/life.js
Normal 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
122
assignments/A4/moarlife.js
Normal 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;
|
136
assignments/A4/spec/life.spec.js
Normal file
136
assignments/A4/spec/life.spec.js
Normal 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
103
assignments/A4/spec/moarlife.spec.js
Normal file
103
assignments/A4/spec/moarlife.spec.js
Normal 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" });
|
||||||
|
});
|
||||||
|
});
|
13
assignments/A4/spec/support/jasmine.json
Normal file
13
assignments/A4/spec/support/jasmine.json
Normal 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
28
assignments/A4/valley.js
Normal 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
8
assignments/A5/.idea/.gitignore
generated
vendored
Normal 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
8
assignments/A5/.idea/A5.iml
generated
Normal 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>
|
6
assignments/A5/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
assignments/A5/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
assignments/A5/.idea/misc.xml
generated
Normal 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
8
assignments/A5/.idea/modules.xml
generated
Normal 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
6
assignments/A5/.idea/vcs.xml
generated
Normal 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
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
122
assignments/A5/readcsv.py
Normal 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
4
assignments/A5/test1.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
name, age, eye colour
|
||||||
|
Bob, 5, blue
|
||||||
|
Mary, 27, brown
|
||||||
|
Vij, 54, green
|
|
3
assignments/A5/test2.csv
Normal file
3
assignments/A5/test2.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
name, 100
|
||||||
|
teddy, 500
|
||||||
|
lovely, 1000
|
|
114
assignments/A5/test_readcsv.py
Normal file
114
assignments/A5/test_readcsv.py
Normal 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"]]
|
1752
assignments/A5/test_readcsv_bigdata.py
Normal file
1752
assignments/A5/test_readcsv_bigdata.py
Normal file
File diff suppressed because it is too large
Load Diff
32
assignments/A6/classify.m
Normal file
32
assignments/A6/classify.m
Normal 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
151
assignments/A6/iris.csv
Normal 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
assignments/A6/workspace.octave-workspace
Normal file
1
assignments/A6/workspace.octave-workspace
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Created by Octave 7.3.0, Wed Dec 07 23:43:44 2022 GMT <unknown@Isaac-DesktopPC>
|
24
labs/L14/.idea/runConfigurations/client.xml
generated
Normal file
24
labs/L14/.idea/runConfigurations/client.xml
generated
Normal 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>
|
24
labs/L14/.idea/runConfigurations/fizzbuzz.xml
generated
Normal file
24
labs/L14/.idea/runConfigurations/fizzbuzz.xml
generated
Normal 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>
|
24
labs/L14/.idea/runConfigurations/humansize.xml
generated
Normal file
24
labs/L14/.idea/runConfigurations/humansize.xml
generated
Normal 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>
|
17
labs/L14/.idea/runConfigurations/pytest.xml
generated
Normal file
17
labs/L14/.idea/runConfigurations/pytest.xml
generated
Normal 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
8
labs/L15/.idea/.gitignore
generated
vendored
Normal 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
8
labs/L15/.idea/L15.iml
generated
Normal 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>
|
6
labs/L15/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
labs/L15/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
labs/L15/.idea/misc.xml
generated
Normal 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
8
labs/L15/.idea/modules.xml
generated
Normal 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>
|
24
labs/L15/.idea/runConfigurations/globex.xml
generated
Normal file
24
labs/L15/.idea/runConfigurations/globex.xml
generated
Normal 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>
|
17
labs/L15/.idea/runConfigurations/pytest.xml
generated
Normal file
17
labs/L15/.idea/runConfigurations/pytest.xml
generated
Normal 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
6
labs/L15/.idea/vcs.xml
generated
Normal 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
22
labs/L15/globex.py
Normal 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
7
labs/L15/list2dict.py
Normal 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
9
labs/L15/test_globex.py
Normal 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)
|
10
labs/L15/test_list2dict.py
Normal file
10
labs/L15/test_list2dict.py
Normal 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
8
labs/L16/.idea/.gitignore
generated
vendored
Normal 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
8
labs/L16/.idea/L16.iml
generated
Normal 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>
|
12
labs/L16/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
labs/L16/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
6
labs/L16/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
labs/L16/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
labs/L16/.idea/misc.xml
generated
Normal 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
8
labs/L16/.idea/modules.xml
generated
Normal 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
6
labs/L16/.idea/vcs.xml
generated
Normal 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
14
labs/L16/parse_csv.py
Normal 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
|
45
labs/L16/test_parse_csv.py
Normal file
45
labs/L16/test_parse_csv.py
Normal 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
8
labs/L17/.idea/.gitignore
generated
vendored
Normal 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
10
labs/L17/.idea/L17.iml
generated
Normal 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>
|
12
labs/L17/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
labs/L17/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
6
labs/L17/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
labs/L17/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
labs/L17/.idea/misc.xml
generated
Normal 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
8
labs/L17/.idea/modules.xml
generated
Normal 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
6
labs/L17/.idea/vcs.xml
generated
Normal 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
16
labs/L17/main.py
Normal 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/
|
22
tests/Final/Q1 Racket/balance.rkt
Normal file
22
tests/Final/Q1 Racket/balance.rkt
Normal 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))
|
19
tests/Final/Q2 Python/invert.py
Normal file
19
tests/Final/Q2 Python/invert.py
Normal 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
|
13
tests/Final/Q2 Python/test_invert.py
Normal file
13
tests/Final/Q2 Python/test_invert.py
Normal 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
|
50
tests/Final/Q3 Javascript/expression.js
Normal file
50
tests/Final/Q3 Javascript/expression.js
Normal 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;
|
71
tests/Final/Q3 Javascript/spec/expression.spec.js
Normal file
71
tests/Final/Q3 Javascript/spec/expression.spec.js
Normal 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
|
||||||
|
});
|
13
tests/Final/Q3 Javascript/spec/support/jasmine.json
Normal file
13
tests/Final/Q3 Javascript/spec/support/jasmine.json
Normal 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
71
tests/Final/tests.txt
Normal 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
4
tests/Q3/prefix.py
Normal 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
41
tests/Q3/test_prefix.py
Normal 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"]
|
32
tests/practice/Q3/practice_questions.py
Normal file
32
tests/practice/Q3/practice_questions.py
Normal 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
|
||||||
|
|
37
tests/practice/Q3/test_practice_questions.py
Normal file
37
tests/practice/Q3/test_practice_questions.py
Normal 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
|
1
utils/python-pytest-script.txt
Normal file
1
utils/python-pytest-script.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pytest --cov --cov-report=term-missing
|
BIN
utils/python-requirements.txt
Normal file
BIN
utils/python-requirements.txt
Normal file
Binary file not shown.
4
utils/python-venv/.gitignore
vendored
4
utils/python-venv/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
# created by virtualenv automatically
|
|
||||||
|
|
||||||
# Commit venv because it is shared between all of python environments for this class
|
|
||||||
# *
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||||||
__import__('_distutils_hack').do_override()
|
|
@ -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]
|
|
@ -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
|
|
@ -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
@ -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
|
|
@ -1,8 +0,0 @@
|
|||||||
from .terminalwriter import get_terminal_width
|
|
||||||
from .terminalwriter import TerminalWriter
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"TerminalWriter",
|
|
||||||
"get_terminal_width",
|
|
||||||
]
|
|
@ -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)
|
|
@ -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")
|
|
||||||
)
|
|
||||||
)
|
|
@ -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
|
|
@ -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
@ -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)
|
|
@ -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)
|
|
File diff suppressed because it is too large
Load Diff
@ -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
|
|
@ -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)
|
|
@ -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
@ -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
@ -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
|
|
@ -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
|
|
@ -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."""
|
|
@ -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 {}
|
|
@ -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")
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
Reference in New Issue
Block a user