Compare commits
20 Commits
a50f49d2c8
...
master
Author | SHA1 | Date | |
---|---|---|---|
4ecaa1a216 | |||
fcef1277f3 | |||
8507082995 | |||
e091a825ce | |||
8501f2f473 | |||
bfdc5dc93b | |||
d81425a781 | |||
62f9f2677d | |||
95918841b0 | |||
419e67f80a | |||
38135762f6 | |||
5c03647a4a | |||
3ee1155e1f | |||
efb36f83fc | |||
d28bd6ed1c | |||
45c5b94e63 | |||
0f1e8d5e36 | |||
57405f5aac | |||
8500f351fc | |||
268e3b6a47 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -2,4 +2,5 @@
|
|||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
|
||||||
*.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>
|
@ -1,9 +1,21 @@
|
|||||||
Title: Lab Fourteen
|
Title: Lab Fourteen
|
||||||
Date: 2022-10-31T08:30:00
|
Date: 2022-10-31T08:30:00
|
||||||
Tags: cs2613, lab, python
|
Tags: cs2613, lab, python, pytest, exceptions, modules
|
||||||
|
|
||||||
Sample description
|
In this lab I learned about python, pytest, python exceptions and modules.
|
||||||
<!-- more -->
|
<!-- more -->
|
||||||
|
|
||||||
## Sample Body
|
## Pytest
|
||||||
Sample Body
|
Pytest is a python testing framework. It can show you code coverage with line counts. This is pretty similar to `nyc` from javascript. You run tests by just running `pytest` or `pytest -cov` for code coverage. Tests are defined by in a file having a function with an assert statement. Pytest will then scan your files for assert statements and run those functions to run tests. Testing in python is pretty simple, like with javascript. Racket testing is a little more abstract and confusing in my opinion.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
In python functions are exported by default, in contrast to javascript. You must guard your main method with the `if __name__ =='__main__'` text so it does not run when the module is imported. You can make tests to ensure you have documentation for your functions, by checking for the imports `__doc__` variable.
|
||||||
|
|
||||||
|
## Indentation
|
||||||
|
In python, blocks are defined by indentation (also known as whitespace). This sets the scope for a given statement. It can be a little confusing to tell if an if statement is in a for loop or not, but linters help.
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
Python can raise errors when things go wrong, for example diving by zero. You can catch these exceptions and handle them if you desire.
|
||||||
|
|
||||||
|
## FizzBuzz missing case
|
||||||
|
You can fix the missing case just by adding another if statement above the other if statement for the fizz case.
|
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()
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user