diff options
Diffstat (limited to 'tests')
62 files changed, 5551 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/_card_render.py b/tests/_card_render.py new file mode 100644 index 0000000..6b7fd17 --- /dev/null +++ b/tests/_card_render.py @@ -0,0 +1 @@ +expected = "\x1b[3m Rich features \x1b[0m\n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Colors \x1b[0m\x1b[1;31m \x1b[0m✓ \x1b[1;32m4-bit color\x1b[0m \x1b[38;2;86;0;0;48;2;51;0;0m▄\x1b[0m\x1b[38;2;86;9;0;48;2;51;5;0m▄\x1b[0m\x1b[38;2;86;18;0;48;2;51;11;0m▄\x1b[0m\x1b[38;2;86;28;0;48;2;51;16;0m▄\x1b[0m\x1b[38;2;86;37;0;48;2;51;22;0m▄\x1b[0m\x1b[38;2;86;47;0;48;2;51;27;0m▄\x1b[0m\x1b[38;2;86;56;0;48;2;51;33;0m▄\x1b[0m\x1b[38;2;86;66;0;48;2;51;38;0m▄\x1b[0m\x1b[38;2;86;75;0;48;2;51;44;0m▄\x1b[0m\x1b[38;2;86;85;0;48;2;51;50;0m▄\x1b[0m\x1b[38;2;78;86;0;48;2;46;51;0m▄\x1b[0m\x1b[38;2;69;86;0;48;2;40;51;0m▄\x1b[0m\x1b[38;2;59;86;0;48;2;35;51;0m▄\x1b[0m\x1b[38;2;50;86;0;48;2;29;51;0m▄\x1b[0m\x1b[38;2;40;86;0;48;2;24;51;0m▄\x1b[0m\x1b[38;2;31;86;0;48;2;18;51;0m▄\x1b[0m\x1b[38;2;22;86;0;48;2;12;51;0m▄\x1b[0m\x1b[38;2;12;86;0;48;2;7;51;0m▄\x1b[0m\x1b[38;2;3;86;0;48;2;1;51;0m▄\x1b[0m\x1b[38;2;0;86;6;48;2;0;51;3m▄\x1b[0m\x1b[38;2;0;86;15;48;2;0;51;9m▄\x1b[0m\x1b[38;2;0;86;25;48;2;0;51;14m▄\x1b[0m\x1b[38;2;0;86;34;48;2;0;51;20m▄\x1b[0m\x1b[38;2;0;86;44;48;2;0;51;25m▄\x1b[0m\x1b[38;2;0;86;53;48;2;0;51;31m▄\x1b[0m\x1b[38;2;0;86;63;48;2;0;51;37m▄\x1b[0m\x1b[38;2;0;86;72;48;2;0;51;42m▄\x1b[0m\x1b[38;2;0;86;81;48;2;0;51;48m▄\x1b[0m\x1b[38;2;0;81;86;48;2;0;48;51m▄\x1b[0m\x1b[38;2;0;72;86;48;2;0;42;51m▄\x1b[0m\x1b[38;2;0;63;86;48;2;0;37;51m▄\x1b[0m\x1b[38;2;0;53;86;48;2;0;31;51m▄\x1b[0m\x1b[38;2;0;44;86;48;2;0;25;51m▄\x1b[0m\x1b[38;2;0;34;86;48;2;0;20;51m▄\x1b[0m\x1b[38;2;0;25;86;48;2;0;14;51m▄\x1b[0m\x1b[38;2;0;15;86;48;2;0;9;51m▄\x1b[0m\x1b[38;2;0;6;86;48;2;0;3;51m▄\x1b[0m\x1b[38;2;3;0;86;48;2;1;0;51m▄\x1b[0m\x1b[38;2;12;0;86;48;2;7;0;51m▄\x1b[0m\x1b[38;2;22;0;86;48;2;12;0;51m▄\x1b[0m\x1b[38;2;31;0;86;48;2;18;0;51m▄\x1b[0m\x1b[38;2;40;0;86;48;2;24;0;51m▄\x1b[0m\x1b[38;2;50;0;86;48;2;29;0;51m▄\x1b[0m\x1b[38;2;59;0;86;48;2;35;0;51m▄\x1b[0m\x1b[38;2;69;0;86;48;2;40;0;51m▄\x1b[0m\x1b[38;2;78;0;86;48;2;46;0;51m▄\x1b[0m\x1b[38;2;86;0;85;48;2;51;0;50m▄\x1b[0m\x1b[38;2;86;0;75;48;2;51;0;44m▄\x1b[0m\x1b[38;2;86;0;66;48;2;51;0;38m▄\x1b[0m\x1b[38;2;86;0;56;48;2;51;0;33m▄\x1b[0m\x1b[38;2;86;0;47;48;2;51;0;27m▄\x1b[0m\x1b[38;2;86;0;37;48;2;51;0;22m▄\x1b[0m\x1b[38;2;86;0;28;48;2;51;0;16m▄\x1b[0m\x1b[38;2;86;0;18;48;2;51;0;11m▄\x1b[0m\x1b[38;2;86;0;9;48;2;51;0;5m▄\x1b[0m \n ✓ \x1b[1;34m8-bit color\x1b[0m \x1b[38;2;158;0;0;48;2;122;0;0m▄\x1b[0m\x1b[38;2;158;17;0;48;2;122;13;0m▄\x1b[0m\x1b[38;2;158;34;0;48;2;122;26;0m▄\x1b[0m\x1b[38;2;158;51;0;48;2;122;40;0m▄\x1b[0m\x1b[38;2;158;68;0;48;2;122;53;0m▄\x1b[0m\x1b[38;2;158;86;0;48;2;122;66;0m▄\x1b[0m\x1b[38;2;158;103;0;48;2;122;80;0m▄\x1b[0m\x1b[38;2;158;120;0;48;2;122;93;0m▄\x1b[0m\x1b[38;2;158;137;0;48;2;122;106;0m▄\x1b[0m\x1b[38;2;158;155;0;48;2;122;120;0m▄\x1b[0m\x1b[38;2;143;158;0;48;2;111;122;0m▄\x1b[0m\x1b[38;2;126;158;0;48;2;97;122;0m▄\x1b[0m\x1b[38;2;109;158;0;48;2;84;122;0m▄\x1b[0m\x1b[38;2;91;158;0;48;2;71;122;0m▄\x1b[0m\x1b[38;2;74;158;0;48;2;57;122;0m▄\x1b[0m\x1b[38;2;57;158;0;48;2;44;122;0m▄\x1b[0m\x1b[38;2;40;158;0;48;2;31;122;0m▄\x1b[0m\x1b[38;2;22;158;0;48;2;17;122;0m▄\x1b[0m\x1b[38;2;5;158;0;48;2;4;122;0m▄\x1b[0m\x1b[38;2;0;158;11;48;2;0;122;8m▄\x1b[0m\x1b[38;2;0;158;28;48;2;0;122;22m▄\x1b[0m\x1b[38;2;0;158;45;48;2;0;122;35m▄\x1b[0m\x1b[38;2;0;158;63;48;2;0;122;48m▄\x1b[0m\x1b[38;2;0;158;80;48;2;0;122;62m▄\x1b[0m\x1b[38;2;0;158;97;48;2;0;122;75m▄\x1b[0m\x1b[38;2;0;158;114;48;2;0;122;89m▄\x1b[0m\x1b[38;2;0;158;132;48;2;0;122;102m▄\x1b[0m\x1b[38;2;0;158;149;48;2;0;122;115m▄\x1b[0m\x1b[38;2;0;149;158;48;2;0;115;122m▄\x1b[0m\x1b[38;2;0;132;158;48;2;0;102;122m▄\x1b[0m\x1b[38;2;0;114;158;48;2;0;89;122m▄\x1b[0m\x1b[38;2;0;97;158;48;2;0;75;122m▄\x1b[0m\x1b[38;2;0;80;158;48;2;0;62;122m▄\x1b[0m\x1b[38;2;0;63;158;48;2;0;48;122m▄\x1b[0m\x1b[38;2;0;45;158;48;2;0;35;122m▄\x1b[0m\x1b[38;2;0;28;158;48;2;0;22;122m▄\x1b[0m\x1b[38;2;0;11;158;48;2;0;8;122m▄\x1b[0m\x1b[38;2;5;0;158;48;2;4;0;122m▄\x1b[0m\x1b[38;2;22;0;158;48;2;17;0;122m▄\x1b[0m\x1b[38;2;40;0;158;48;2;31;0;122m▄\x1b[0m\x1b[38;2;57;0;158;48;2;44;0;122m▄\x1b[0m\x1b[38;2;74;0;158;48;2;57;0;122m▄\x1b[0m\x1b[38;2;91;0;158;48;2;71;0;122m▄\x1b[0m\x1b[38;2;109;0;158;48;2;84;0;122m▄\x1b[0m\x1b[38;2;126;0;158;48;2;97;0;122m▄\x1b[0m\x1b[38;2;143;0;158;48;2;111;0;122m▄\x1b[0m\x1b[38;2;158;0;155;48;2;122;0;120m▄\x1b[0m\x1b[38;2;158;0;137;48;2;122;0;106m▄\x1b[0m\x1b[38;2;158;0;120;48;2;122;0;93m▄\x1b[0m\x1b[38;2;158;0;103;48;2;122;0;80m▄\x1b[0m\x1b[38;2;158;0;86;48;2;122;0;66m▄\x1b[0m\x1b[38;2;158;0;68;48;2;122;0;53m▄\x1b[0m\x1b[38;2;158;0;51;48;2;122;0;40m▄\x1b[0m\x1b[38;2;158;0;34;48;2;122;0;26m▄\x1b[0m\x1b[38;2;158;0;17;48;2;122;0;13m▄\x1b[0m \n ✓ \x1b[1;35mTruecolor (16.7 million)\x1b[0m \x1b[38;2;229;0;0;48;2;193;0;0m▄\x1b[0m\x1b[38;2;229;25;0;48;2;193;21;0m▄\x1b[0m\x1b[38;2;229;50;0;48;2;193;42;0m▄\x1b[0m\x1b[38;2;229;75;0;48;2;193;63;0m▄\x1b[0m\x1b[38;2;229;100;0;48;2;193;84;0m▄\x1b[0m\x1b[38;2;229;125;0;48;2;193;105;0m▄\x1b[0m\x1b[38;2;229;150;0;48;2;193;126;0m▄\x1b[0m\x1b[38;2;229;175;0;48;2;193;147;0m▄\x1b[0m\x1b[38;2;229;200;0;48;2;193;169;0m▄\x1b[0m\x1b[38;2;229;225;0;48;2;193;190;0m▄\x1b[0m\x1b[38;2;208;229;0;48;2;176;193;0m▄\x1b[0m\x1b[38;2;183;229;0;48;2;155;193;0m▄\x1b[0m\x1b[38;2;158;229;0;48;2;133;193;0m▄\x1b[0m\x1b[38;2;133;229;0;48;2;112;193;0m▄\x1b[0m\x1b[38;2;108;229;0;48;2;91;193;0m▄\x1b[0m\x1b[38;2;83;229;0;48;2;70;193;0m▄\x1b[0m\x1b[38;2;58;229;0;48;2;49;193;0m▄\x1b[0m\x1b[38;2;33;229;0;48;2;28;193;0m▄\x1b[0m\x1b[38;2;8;229;0;48;2;7;193;0m▄\x1b[0m\x1b[38;2;0;229;16;48;2;0;193;14m▄\x1b[0m\x1b[38;2;0;229;41;48;2;0;193;35m▄\x1b[0m\x1b[38;2;0;229;66;48;2;0;193;56m▄\x1b[0m\x1b[38;2;0;229;91;48;2;0;193;77m▄\x1b[0m\x1b[38;2;0;229;116;48;2;0;193;98m▄\x1b[0m\x1b[38;2;0;229;141;48;2;0;193;119m▄\x1b[0m\x1b[38;2;0;229;166;48;2;0;193;140m▄\x1b[0m\x1b[38;2;0;229;191;48;2;0;193;162m▄\x1b[0m\x1b[38;2;0;229;216;48;2;0;193;183m▄\x1b[0m\x1b[38;2;0;216;229;48;2;0;183;193m▄\x1b[0m\x1b[38;2;0;191;229;48;2;0;162;193m▄\x1b[0m\x1b[38;2;0;166;229;48;2;0;140;193m▄\x1b[0m\x1b[38;2;0;141;229;48;2;0;119;193m▄\x1b[0m\x1b[38;2;0;116;229;48;2;0;98;193m▄\x1b[0m\x1b[38;2;0;91;229;48;2;0;77;193m▄\x1b[0m\x1b[38;2;0;66;229;48;2;0;56;193m▄\x1b[0m\x1b[38;2;0;41;229;48;2;0;35;193m▄\x1b[0m\x1b[38;2;0;16;229;48;2;0;14;193m▄\x1b[0m\x1b[38;2;8;0;229;48;2;7;0;193m▄\x1b[0m\x1b[38;2;33;0;229;48;2;28;0;193m▄\x1b[0m\x1b[38;2;58;0;229;48;2;49;0;193m▄\x1b[0m\x1b[38;2;83;0;229;48;2;70;0;193m▄\x1b[0m\x1b[38;2;108;0;229;48;2;91;0;193m▄\x1b[0m\x1b[38;2;133;0;229;48;2;112;0;193m▄\x1b[0m\x1b[38;2;158;0;229;48;2;133;0;193m▄\x1b[0m\x1b[38;2;183;0;229;48;2;155;0;193m▄\x1b[0m\x1b[38;2;208;0;229;48;2;176;0;193m▄\x1b[0m\x1b[38;2;229;0;225;48;2;193;0;190m▄\x1b[0m\x1b[38;2;229;0;200;48;2;193;0;169m▄\x1b[0m\x1b[38;2;229;0;175;48;2;193;0;147m▄\x1b[0m\x1b[38;2;229;0;150;48;2;193;0;126m▄\x1b[0m\x1b[38;2;229;0;125;48;2;193;0;105m▄\x1b[0m\x1b[38;2;229;0;100;48;2;193;0;84m▄\x1b[0m\x1b[38;2;229;0;75;48;2;193;0;63m▄\x1b[0m\x1b[38;2;229;0;50;48;2;193;0;42m▄\x1b[0m\x1b[38;2;229;0;25;48;2;193;0;21m▄\x1b[0m \n ✓ \x1b[1;33mDumb terminals\x1b[0m \x1b[38;2;254;45;45;48;2;255;10;10m▄\x1b[0m\x1b[38;2;254;68;45;48;2;255;36;10m▄\x1b[0m\x1b[38;2;254;91;45;48;2;255;63;10m▄\x1b[0m\x1b[38;2;254;114;45;48;2;255;90;10m▄\x1b[0m\x1b[38;2;254;137;45;48;2;255;117;10m▄\x1b[0m\x1b[38;2;254;159;45;48;2;255;143;10m▄\x1b[0m\x1b[38;2;254;182;45;48;2;255;170;10m▄\x1b[0m\x1b[38;2;254;205;45;48;2;255;197;10m▄\x1b[0m\x1b[38;2;254;228;45;48;2;255;223;10m▄\x1b[0m\x1b[38;2;254;251;45;48;2;255;250;10m▄\x1b[0m\x1b[38;2;235;254;45;48;2;232;255;10m▄\x1b[0m\x1b[38;2;213;254;45;48;2;206;255;10m▄\x1b[0m\x1b[38;2;190;254;45;48;2;179;255;10m▄\x1b[0m\x1b[38;2;167;254;45;48;2;152;255;10m▄\x1b[0m\x1b[38;2;144;254;45;48;2;125;255;10m▄\x1b[0m\x1b[38;2;121;254;45;48;2;99;255;10m▄\x1b[0m\x1b[38;2;99;254;45;48;2;72;255;10m▄\x1b[0m\x1b[38;2;76;254;45;48;2;45;255;10m▄\x1b[0m\x1b[38;2;53;254;45;48;2;19;255;10m▄\x1b[0m\x1b[38;2;45;254;61;48;2;10;255;28m▄\x1b[0m\x1b[38;2;45;254;83;48;2;10;255;54m▄\x1b[0m\x1b[38;2;45;254;106;48;2;10;255;81m▄\x1b[0m\x1b[38;2;45;254;129;48;2;10;255;108m▄\x1b[0m\x1b[38;2;45;254;152;48;2;10;255;134m▄\x1b[0m\x1b[38;2;45;254;175;48;2;10;255;161m▄\x1b[0m\x1b[38;2;45;254;197;48;2;10;255;188m▄\x1b[0m\x1b[38;2;45;254;220;48;2;10;255;214m▄\x1b[0m\x1b[38;2;45;254;243;48;2;10;255;241m▄\x1b[0m\x1b[38;2;45;243;254;48;2;10;241;255m▄\x1b[0m\x1b[38;2;45;220;254;48;2;10;214;255m▄\x1b[0m\x1b[38;2;45;197;254;48;2;10;188;255m▄\x1b[0m\x1b[38;2;45;175;254;48;2;10;161;255m▄\x1b[0m\x1b[38;2;45;152;254;48;2;10;134;255m▄\x1b[0m\x1b[38;2;45;129;254;48;2;10;108;255m▄\x1b[0m\x1b[38;2;45;106;254;48;2;10;81;255m▄\x1b[0m\x1b[38;2;45;83;254;48;2;10;54;255m▄\x1b[0m\x1b[38;2;45;61;254;48;2;10;28;255m▄\x1b[0m\x1b[38;2;53;45;254;48;2;19;10;255m▄\x1b[0m\x1b[38;2;76;45;254;48;2;45;10;255m▄\x1b[0m\x1b[38;2;99;45;254;48;2;72;10;255m▄\x1b[0m\x1b[38;2;121;45;254;48;2;99;10;255m▄\x1b[0m\x1b[38;2;144;45;254;48;2;125;10;255m▄\x1b[0m\x1b[38;2;167;45;254;48;2;152;10;255m▄\x1b[0m\x1b[38;2;190;45;254;48;2;179;10;255m▄\x1b[0m\x1b[38;2;213;45;254;48;2;206;10;255m▄\x1b[0m\x1b[38;2;235;45;254;48;2;232;10;255m▄\x1b[0m\x1b[38;2;254;45;251;48;2;255;10;250m▄\x1b[0m\x1b[38;2;254;45;228;48;2;255;10;223m▄\x1b[0m\x1b[38;2;254;45;205;48;2;255;10;197m▄\x1b[0m\x1b[38;2;254;45;182;48;2;255;10;170m▄\x1b[0m\x1b[38;2;254;45;159;48;2;255;10;143m▄\x1b[0m\x1b[38;2;254;45;137;48;2;255;10;117m▄\x1b[0m\x1b[38;2;254;45;114;48;2;255;10;90m▄\x1b[0m\x1b[38;2;254;45;91;48;2;255;10;63m▄\x1b[0m\x1b[38;2;254;45;68;48;2;255;10;36m▄\x1b[0m \n ✓ \x1b[1;36mAutomatic color conversion\x1b[0m \x1b[38;2;255;117;117;48;2;255;81;81m▄\x1b[0m\x1b[38;2;255;132;117;48;2;255;100;81m▄\x1b[0m\x1b[38;2;255;147;117;48;2;255;119;81m▄\x1b[0m\x1b[38;2;255;162;117;48;2;255;138;81m▄\x1b[0m\x1b[38;2;255;177;117;48;2;255;157;81m▄\x1b[0m\x1b[38;2;255;192;117;48;2;255;176;81m▄\x1b[0m\x1b[38;2;255;207;117;48;2;255;195;81m▄\x1b[0m\x1b[38;2;255;222;117;48;2;255;214;81m▄\x1b[0m\x1b[38;2;255;237;117;48;2;255;232;81m▄\x1b[0m\x1b[38;2;255;252;117;48;2;255;251;81m▄\x1b[0m\x1b[38;2;242;255;117;48;2;239;255;81m▄\x1b[0m\x1b[38;2;227;255;117;48;2;220;255;81m▄\x1b[0m\x1b[38;2;212;255;117;48;2;201;255;81m▄\x1b[0m\x1b[38;2;197;255;117;48;2;182;255;81m▄\x1b[0m\x1b[38;2;182;255;117;48;2;163;255;81m▄\x1b[0m\x1b[38;2;167;255;117;48;2;144;255;81m▄\x1b[0m\x1b[38;2;152;255;117;48;2;125;255;81m▄\x1b[0m\x1b[38;2;137;255;117;48;2;106;255;81m▄\x1b[0m\x1b[38;2;122;255;117;48;2;87;255;81m▄\x1b[0m\x1b[38;2;117;255;127;48;2;81;255;94m▄\x1b[0m\x1b[38;2;117;255;142;48;2;81;255;113m▄\x1b[0m\x1b[38;2;117;255;157;48;2;81;255;132m▄\x1b[0m\x1b[38;2;117;255;172;48;2;81;255;150m▄\x1b[0m\x1b[38;2;117;255;187;48;2;81;255;169m▄\x1b[0m\x1b[38;2;117;255;202;48;2;81;255;188m▄\x1b[0m\x1b[38;2;117;255;217;48;2;81;255;207m▄\x1b[0m\x1b[38;2;117;255;232;48;2;81;255;226m▄\x1b[0m\x1b[38;2;117;255;247;48;2;81;255;245m▄\x1b[0m\x1b[38;2;117;247;255;48;2;81;245;255m▄\x1b[0m\x1b[38;2;117;232;255;48;2;81;226;255m▄\x1b[0m\x1b[38;2;117;217;255;48;2;81;207;255m▄\x1b[0m\x1b[38;2;117;202;255;48;2;81;188;255m▄\x1b[0m\x1b[38;2;117;187;255;48;2;81;169;255m▄\x1b[0m\x1b[38;2;117;172;255;48;2;81;150;255m▄\x1b[0m\x1b[38;2;117;157;255;48;2;81;132;255m▄\x1b[0m\x1b[38;2;117;142;255;48;2;81;113;255m▄\x1b[0m\x1b[38;2;117;127;255;48;2;81;94;255m▄\x1b[0m\x1b[38;2;122;117;255;48;2;87;81;255m▄\x1b[0m\x1b[38;2;137;117;255;48;2;106;81;255m▄\x1b[0m\x1b[38;2;152;117;255;48;2;125;81;255m▄\x1b[0m\x1b[38;2;167;117;255;48;2;144;81;255m▄\x1b[0m\x1b[38;2;182;117;255;48;2;163;81;255m▄\x1b[0m\x1b[38;2;197;117;255;48;2;182;81;255m▄\x1b[0m\x1b[38;2;212;117;255;48;2;201;81;255m▄\x1b[0m\x1b[38;2;227;117;255;48;2;220;81;255m▄\x1b[0m\x1b[38;2;242;117;255;48;2;239;81;255m▄\x1b[0m\x1b[38;2;255;117;252;48;2;255;81;251m▄\x1b[0m\x1b[38;2;255;117;237;48;2;255;81;232m▄\x1b[0m\x1b[38;2;255;117;222;48;2;255;81;214m▄\x1b[0m\x1b[38;2;255;117;207;48;2;255;81;195m▄\x1b[0m\x1b[38;2;255;117;192;48;2;255;81;176m▄\x1b[0m\x1b[38;2;255;117;177;48;2;255;81;157m▄\x1b[0m\x1b[38;2;255;117;162;48;2;255;81;138m▄\x1b[0m\x1b[38;2;255;117;147;48;2;255;81;119m▄\x1b[0m\x1b[38;2;255;117;132;48;2;255;81;100m▄\x1b[0m \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Styles \x1b[0m\x1b[1;31m \x1b[0mAll ansi styles: \x1b[1mbold\x1b[0m, \x1b[2mdim\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[9mstrikethrough\x1b[0m, \x1b[7mreverse\x1b[0m, and even \n \x1b[5mblink\x1b[0m. \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Text \x1b[0m\x1b[1;31m \x1b[0mWord wrap text. Justify \x1b[32mleft\x1b[0m, \x1b[33mcenter\x1b[0m, \x1b[34mright\x1b[0m or \x1b[31mfull\x1b[0m. \n \n \x1b[32mLorem ipsum dolor \x1b[0m \x1b[33m Lorem ipsum dolor \x1b[0m \x1b[34m Lorem ipsum dolor\x1b[0m \x1b[31mLorem\x1b[0m\x1b[31m \x1b[0m\x1b[31mipsum\x1b[0m\x1b[31m \x1b[0m\x1b[31mdolor\x1b[0m\x1b[31m \x1b[0m\x1b[31msit\x1b[0m \n \x1b[32msit amet, \x1b[0m \x1b[33m sit amet, \x1b[0m \x1b[34m sit amet,\x1b[0m \x1b[31mamet,\x1b[0m\x1b[31m \x1b[0m\x1b[31mconsectetur\x1b[0m \n \x1b[32mconsectetur \x1b[0m \x1b[33m consectetur \x1b[0m \x1b[34m consectetur\x1b[0m \x1b[31madipiscing\x1b[0m\x1b[31m \x1b[0m\x1b[31melit.\x1b[0m \n \x1b[32madipiscing elit. \x1b[0m \x1b[33m adipiscing elit. \x1b[0m \x1b[34m adipiscing elit.\x1b[0m \x1b[31mQuisque\x1b[0m\x1b[31m \x1b[0m\x1b[31min\x1b[0m\x1b[31m \x1b[0m\x1b[31mmetus\x1b[0m\x1b[31m \x1b[0m\x1b[31msed\x1b[0m \n \x1b[32mQuisque in metus sed\x1b[0m \x1b[33mQuisque in metus sed\x1b[0m \x1b[34mQuisque in metus sed\x1b[0m \x1b[31msapien\x1b[0m\x1b[31m \x1b[0m\x1b[31multricies\x1b[0m \n \x1b[32msapien ultricies \x1b[0m \x1b[33m sapien ultricies \x1b[0m \x1b[34m sapien ultricies\x1b[0m \x1b[31mpretium\x1b[0m\x1b[31m \x1b[0m\x1b[31ma\x1b[0m\x1b[31m \x1b[0m\x1b[31mat\x1b[0m\x1b[31m \x1b[0m\x1b[31mjusto.\x1b[0m \n \x1b[32mpretium a at justo. \x1b[0m \x1b[33mpretium a at justo. \x1b[0m \x1b[34m pretium a at justo.\x1b[0m \x1b[31mMaecenas\x1b[0m\x1b[31m \x1b[0m\x1b[31mluctus\x1b[0m\x1b[31m \x1b[0m\x1b[31mvelit\x1b[0m \n \x1b[32mMaecenas luctus \x1b[0m \x1b[33m Maecenas luctus \x1b[0m \x1b[34m Maecenas luctus\x1b[0m \x1b[31met auctor maximus.\x1b[0m \n \x1b[32mvelit et auctor \x1b[0m \x1b[33m velit et auctor \x1b[0m \x1b[34m velit et auctor\x1b[0m \n \x1b[32mmaximus. \x1b[0m \x1b[33m maximus. \x1b[0m \x1b[34m maximus.\x1b[0m \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Asian \x1b[0m\x1b[1;31m \x1b[0m🇨🇳 该库支持中文,日文和韩文文本! \n\x1b[1;31m \x1b[0m\x1b[1;31m language \x1b[0m\x1b[1;31m \x1b[0m🇯🇵 ライブラリは中国語、日本語、韓国語のテキストをサポートしています \n\x1b[1;31m \x1b[0m\x1b[1;31m support \x1b[0m\x1b[1;31m \x1b[0m🇰🇷 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다 \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Markup \x1b[0m\x1b[1;31m \x1b[0m\x1b[1;35mRich\x1b[0m supports a simple \x1b[3mbbcode\x1b[0m like \x1b[1mmarkup\x1b[0m for \x1b[33mcolor\x1b[0m, \x1b[4mstyle\x1b[0m, and emoji! 👍 🍎 🐜 🐻 … \n 🚌 \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Tables \x1b[0m\x1b[1;31m \x1b[0m\x1b[1m \x1b[0m\x1b[1;32mDate\x1b[0m\x1b[1m \x1b[0m\x1b[1m \x1b[0m \x1b[1m \x1b[0m\x1b[1;34mTitle\x1b[0m\x1b[1m \x1b[0m\x1b[1m \x1b[0m \x1b[1m \x1b[0m\x1b[1;36mProduction Budget\x1b[0m\x1b[1m \x1b[0m \x1b[1m \x1b[0m\x1b[1m \x1b[0m\x1b[1;35mBox Office\x1b[0m\x1b[1m \x1b[0m \n ───────────────────────────────────────────────────────────────────────────────────── \n \x1b[32m \x1b[0m\x1b[32mDec 20, 2019\x1b[0m\x1b[32m \x1b[0m \x1b[34m \x1b[0m\x1b[34mStar Wars: The Rise of \x1b[0m\x1b[34m \x1b[0m \x1b[36m \x1b[0m\x1b[36m $275,000,000\x1b[0m\x1b[36m \x1b[0m \x1b[35m \x1b[0m\x1b[35m $375,126,118\x1b[0m\x1b[35m \x1b[0m \n \x1b[34m \x1b[0m\x1b[34mSkywalker \x1b[0m\x1b[34m \x1b[0m \n \x1b[2;32m \x1b[0m\x1b[2;32mMay 25, 2018\x1b[0m\x1b[2;32m \x1b[0m \x1b[2;34m \x1b[0m\x1b[1;2;34mSolo\x1b[0m\x1b[2;34m: A Star Wars Story \x1b[0m\x1b[2;34m \x1b[0m \x1b[2;36m \x1b[0m\x1b[2;36m $275,000,000\x1b[0m\x1b[2;36m \x1b[0m \x1b[2;35m \x1b[0m\x1b[2;35m $393,151,347\x1b[0m\x1b[2;35m \x1b[0m \n \x1b[32m \x1b[0m\x1b[32mDec 15, 2017\x1b[0m\x1b[32m \x1b[0m \x1b[34m \x1b[0m\x1b[34mStar Wars Ep. VIII: The Last \x1b[0m\x1b[34m \x1b[0m \x1b[36m \x1b[0m\x1b[36m $262,000,000\x1b[0m\x1b[36m \x1b[0m \x1b[35m \x1b[0m\x1b[1;35m$1,332,539,889\x1b[0m\x1b[35m \x1b[0m \n \x1b[34m \x1b[0m\x1b[34mJedi \x1b[0m\x1b[34m \x1b[0m \n \x1b[2;32m \x1b[0m\x1b[2;32mMay 19, 1999\x1b[0m\x1b[2;32m \x1b[0m \x1b[2;34m \x1b[0m\x1b[2;34mStar Wars Ep. \x1b[0m\x1b[1;2;34mI\x1b[0m\x1b[2;34m: \x1b[0m\x1b[2;3;34mThe phantom \x1b[0m\x1b[2;34m \x1b[0m\x1b[2;34m \x1b[0m \x1b[2;36m \x1b[0m\x1b[2;36m $115,000,000\x1b[0m\x1b[2;36m \x1b[0m \x1b[2;35m \x1b[0m\x1b[2;35m$1,027,044,677\x1b[0m\x1b[2;35m \x1b[0m \n \x1b[2m \x1b[0m \x1b[2;34m \x1b[0m\x1b[2;3;34mMenace\x1b[0m\x1b[2;34m \x1b[0m\x1b[2;34m \x1b[0m \x1b[2m \x1b[0m \x1b[2m \x1b[0m \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Syntax \x1b[0m\x1b[1;31m \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 1 \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mdef\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34miter_last\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mvalues\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mIterable\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m[\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mT\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m]\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m-\x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m>\x1b[0m \x1b[1m{\x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31mhighlighting\x1b[0m\x1b[1;31m \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 2 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;230;219;116;48;2;39;40;34m\"\"\"Iterate and generate a tuple w\x1b[0m \x1b[2;32m│ \x1b[0m\x1b[32m'foo'\x1b[0m: \x1b[1m[\x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m & \x1b[0m\x1b[1;31m \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 3 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34miter_values\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m=\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34miter\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mvalues\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ │ \x1b[0m\x1b[1;34m3.1427\x1b[0m, \n\x1b[1;31m \x1b[0m\x1b[1;31m pretty \x1b[0m\x1b[1;31m \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 4 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mtry\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ │ \x1b[0m\x1b[1m(\x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m printing \x1b[0m\x1b[1;31m \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 5 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ │ \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mprevious_value\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m=\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mnext\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34miter_va\x1b[0m \x1b[2;32m│ │ │ \x1b[0m\x1b[32m'Paul Atriedies'\x1b[0m, \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 6 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mexcept\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34mStopIteration\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ │ │ \x1b[0m\x1b[32m'Vladimir Harkonnen'\x1b[0m, \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 7 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ │ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mreturn\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ │ │ \x1b[0m\x1b[32m'Thufir Haway'\x1b[0m \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 8 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mfor\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mvalue\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34min\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34miter_values\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ │ \x1b[0m\x1b[1m)\x1b[0m \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 9 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ │ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34myield\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mFalse\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m,\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mprevious_value\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ \x1b[0m\x1b[1m]\x1b[0m, \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m10 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ │ \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mprevious_value\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m=\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mvalue\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2;32m│ \x1b[0m\x1b[32m'atomic'\x1b[0m: \x1b[1m(\x1b[0m\x1b[3;91mFalse\x1b[0m, \x1b[3;92mTrue\x1b[0m, \x1b[3;35mNone\x1b[0m\x1b[1m)\x1b[0m \n \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m11 \x1b[0m\x1b[2;38;2;117;113;94;48;2;39;40;34m│ \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34myield\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mTrue\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m,\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mprevious_value\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[1m}\x1b[0m \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m Markdown \x1b[0m\x1b[1;31m \x1b[0m\x1b[36m# Markdown\x1b[0m ╔═══════════════════════════════════════╗ \n ║ \x1b[1mMarkdown\x1b[0m ║ \n \x1b[36mSupports much of the *markdown*, \x1b[0m ╚═══════════════════════════════════════╝ \n \x1b[36m__syntax__!\x1b[0m \n Supports much of the \x1b[3mmarkdown\x1b[0m, \x1b[1msyntax\x1b[0m! \n \x1b[36m- Headers\x1b[0m \n \x1b[36m- Basic formatting: **bold**, *italic*, \x1b[0m \x1b[1;33m • \x1b[0mHeaders \n \x1b[36m`code`\x1b[0m \x1b[1;33m • \x1b[0mBasic formatting: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[97;40mcode\x1b[0m \n \x1b[36m- Block quotes\x1b[0m \x1b[1;33m • \x1b[0mBlock quotes \n \x1b[36m- Lists, and more...\x1b[0m \x1b[1;33m • \x1b[0mLists, and more... \n \x1b[36m \x1b[0m \n\x1b[1;31m \x1b[0m \n\x1b[1;31m \x1b[0m\x1b[1;31m +more! \x1b[0m\x1b[1;31m \x1b[0mProgress bars, columns, styled logging handler, tracebacks, etc... \n\x1b[1;31m \x1b[0m \n" diff --git a/tests/_exception_render.py b/tests/_exception_render.py new file mode 100644 index 0000000..15fad3d --- /dev/null +++ b/tests/_exception_render.py @@ -0,0 +1 @@ +expected = '\x1b[1mTraceback\x1b[0m \x1b[2m(most recent call last):\x1b[0m\n\x1b[34m╭──────────────────────────────────────────────────────────────────────────────────────╮\x1b[0m\n\x1b[34m│\x1b[0m File \x1b[32m"test_traceback.py"\x1b[0m, line \x1b[1;36m24\x1b[0m, in \x1b[33mget_exception\x1b[0m \x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m21 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mtry\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m22 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mtry\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m23 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mfoo\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;174;129;255;48;2;39;40;34m0\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[38;2;101;102;96;48;2;39;40;34m❱ \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m24 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mexcept\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m25 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mfoobarbaz\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m26 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mexcept\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m27 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mtb\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m=\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mTraceback\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m File \x1b[32m"test_traceback.py"\x1b[0m, line \x1b[1;36m20\x1b[0m, in \x1b[33mfoo\x1b[0m \x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m17 \x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m18 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mdef\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34mfoo\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m19 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mbar\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[38;2;101;102;96;48;2;39;40;34m❱ \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m20 \x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m21 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mtry\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m22 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mtry\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m23 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mfoo\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;174;129;255;48;2;39;40;34m0\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m File \x1b[32m"test_traceback.py"\x1b[0m, line \x1b[1;36m17\x1b[0m, in \x1b[33mbar\x1b[0m \x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m14 \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mdef\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34mget_exception\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m-\x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m>\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mTraceback\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m15 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mdef\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34mbar\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m16 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mprint\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;174;129;255;48;2;39;40;34m1\x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m/\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[38;2;101;102;96;48;2;39;40;34m❱ \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m17 \x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m18 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mdef\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;166;226;46;48;2;39;40;34mfoo\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m19 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mbar\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34ma\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m20 \x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m╰──────────────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n\x1b[1;38;5;9mZeroDivisionError: \x1b[0mdivision by zero\n\n\x1b[3mDuring handling of the above exception, another exception occurred:\x1b[0m\n\n\x1b[1mTraceback\x1b[0m \x1b[2m(most recent call last):\x1b[0m\n\x1b[34m╭──────────────────────────────────────────────────────────────────────────────────────╮\x1b[0m\n\x1b[34m│\x1b[0m File \x1b[32m"test_traceback.py"\x1b[0m, line \x1b[1;36m26\x1b[0m, in \x1b[33mget_exception\x1b[0m \x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m23 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mfoo\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;174;129;255;48;2;39;40;34m0\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m24 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mexcept\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m25 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mfoobarbaz\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[38;2;101;102;96;48;2;39;40;34m❱ \x1b[0m\x1b[1;38;2;227;227;221;48;2;39;40;34m26 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mexcept\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m:\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m27 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mtb\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;249;38;114;48;2;39;40;34m=\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mTraceback\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m(\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m)\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m28 \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;102;217;239;48;2;39;40;34mreturn\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mtb\x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m│\x1b[0m \x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m29 \x1b[0m\x1b[48;2;39;40;34m \x1b[0m\x1b[34m│\x1b[0m\n\x1b[34m╰──────────────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n\x1b[1;38;5;9mNameError: \x1b[0mname \x1b[32m\'foobarbaz\'\x1b[0m is not defined\n' diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..5496d46 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +junit_family=legacy diff --git a/tests/render.py b/tests/render.py new file mode 100644 index 0000000..a2435c5 --- /dev/null +++ b/tests/render.py @@ -0,0 +1,24 @@ +import io +import re + +from rich.console import Console, RenderableType + + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +def render(renderable: RenderableType, no_wrap: bool = False) -> str: + console = Console( + width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False + ) + console.print(renderable, no_wrap=no_wrap) + output = replace_link_ids(console.file.getvalue()) + return output diff --git a/tests/test_align.py b/tests/test_align.py new file mode 100644 index 0000000..6473334 --- /dev/null +++ b/tests/test_align.py @@ -0,0 +1,146 @@ +import io + +import pytest + +from rich.console import Console +from rich.align import Align, VerticalCenter +from rich.measure import Measurement + + +def test_bad_align_legal(): + + # Legal + Align("foo", "left") + Align("foo", "center") + Align("foo", "right") + + # illegal + with pytest.raises(ValueError): + Align("foo", None) + with pytest.raises(ValueError): + Align("foo", "middle") + with pytest.raises(ValueError): + Align("foo", "") + with pytest.raises(ValueError): + Align("foo", "LEFT") + with pytest.raises(ValueError): + Align("foo", vertical="somewhere") + + +def test_repr(): + repr(Align("foo", "left")) + repr(Align("foo", "center")) + repr(Align("foo", "right")) + + +def test_align_left(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", "left")) + assert console.file.getvalue() == "foo \n" + + +def test_align_center(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", "center")) + assert console.file.getvalue() == " foo \n" + + +def test_align_right(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", "right")) + assert console.file.getvalue() == " foo\n" + + +def test_align_top(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", vertical="top"), height=5) + expected = "foo \n \n \n \n \n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_align_middle(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", vertical="middle"), height=5) + expected = " \n \nfoo \n \n \n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_align_bottom(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", vertical="bottom"), height=5) + expected = " \n \n \n \nfoo \n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_align_center_middle(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo\nbar", "center", vertical="middle"), height=5) + expected = " \n foo \n bar \n \n \n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_align_fit(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foobarbaze", "center")) + assert console.file.getvalue() == "foobarbaze\n" + + +def test_align_right_style(): + console = Console( + file=io.StringIO(), width=10, color_system="truecolor", force_terminal=True + ) + console.print(Align("foo", "right", style="on blue")) + assert console.file.getvalue() == "\x1b[44m \x1b[0m\x1b[44mfoo\x1b[0m\n" + + +def test_measure(): + console = Console(file=io.StringIO(), width=20) + _min, _max = Measurement.get(console, Align("foo bar", "left"), 20) + assert _min == 3 + assert _max == 7 + + +def test_align_no_pad(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", "center", pad=False)) + console.print(Align("foo", "left", pad=False)) + assert console.file.getvalue() == " foo\nfoo\n" + + +def test_align_width(): + console = Console(file=io.StringIO(), width=40) + words = "Deep in the human unconscious is a pervasive need for a logical universe that makes sense. But the real universe is always one step beyond logic" + console.print(Align(words, "center", width=30)) + result = console.file.getvalue() + expected = " Deep in the human unconscious \n is a pervasive need for a \n logical universe that makes \n sense. But the real universe \n is always one step beyond \n logic \n" + assert result == expected + + +def test_shortcuts(): + assert Align.left("foo").align == "left" + assert Align.left("foo").renderable == "foo" + assert Align.right("foo").align == "right" + assert Align.right("foo").renderable == "foo" + assert Align.center("foo").align == "center" + assert Align.center("foo").renderable == "foo" + + +def test_vertical_center(): + console = Console(color_system=None, height=6) + console.begin_capture() + vertical_center = VerticalCenter("foo") + repr(vertical_center) + console.print(vertical_center) + result = console.end_capture() + print(repr(result)) + expected = " \n \nfoo\n \n \n \n" + assert result == expected + assert Measurement.get(console, vertical_center) == Measurement(3, 3) diff --git a/tests/test_ansi.py b/tests/test_ansi.py new file mode 100644 index 0000000..898286c --- /dev/null +++ b/tests/test_ansi.py @@ -0,0 +1,32 @@ +import io + +from rich.ansi import AnsiDecoder +from rich.console import Console +from rich.style import Style +from rich.text import Span, Text + + +def test_decode(): + console = Console( + force_terminal=True, legacy_windows=False, color_system="truecolor" + ) + console.begin_capture() + console.print("Hello") + console.print("[b]foo[/b]") + console.print("[link http://example.org]bar") + console.print("[#ff0000 on color(200)]red") + console.print("[color(200) on #ff0000]red") + terminal_codes = console.end_capture() + + decoder = AnsiDecoder() + lines = list(decoder.decode(terminal_codes)) + + expected = [ + Text("Hello"), + Text("foo", spans=[Span(0, 3, Style.parse("bold"))]), + Text("bar", spans=[Span(0, 3, Style.parse("link http://example.org"))]), + Text("red", spans=[Span(0, 3, Style.parse("#ff0000 on color(200)"))]), + Text("red", spans=[Span(0, 3, Style.parse("color(200) on #ff0000"))]), + ] + + assert lines == expected diff --git a/tests/test_bar.py b/tests/test_bar.py new file mode 100644 index 0000000..46f8e4e --- /dev/null +++ b/tests/test_bar.py @@ -0,0 +1,99 @@ +from rich.progress_bar import ProgressBar +from rich.segment import Segment +from rich.style import Style + +from .render import render + + +def test_init(): + bar = ProgressBar(completed=50) + repr(bar) + assert bar.percentage_completed == 50.0 + + +def test_update(): + bar = ProgressBar() + assert bar.completed == 0 + assert bar.total == 100 + bar.update(10, 20) + assert bar.completed == 10 + assert bar.total == 20 + assert bar.percentage_completed == 50 + bar.update(100) + assert bar.percentage_completed == 100 + + +expected = [ + "\x1b[38;2;249;38;114m━━━━━\x1b[0m\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m", + "\x1b[38;2;249;38;114m━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m", +] + + +def test_render(): + bar = ProgressBar(completed=11, width=50) + bar_render = render(bar) + assert bar_render == expected[0] + bar.update(completed=12) + bar_render = render(bar) + assert bar_render == expected[1] + + +def test_measure(): + bar = ProgressBar() + measurement = bar.__rich_measure__(None, 120) + assert measurement.minimum == 4 + assert measurement.maximum == 120 + + +def test_zero_total(): + # Shouldn't throw zero division error + bar = ProgressBar(total=0) + render(bar) + + +def test_pulse(): + bar = ProgressBar(pulse=True, animation_time=10) + bar_render = render(bar) + print(repr(bar_render)) + expected = "\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m" + assert bar_render == expected + + +def test_get_pulse_segments(): + bar = ProgressBar() + segments = bar._get_pulse_segments( + Style.parse("red"), Style.parse("yellow"), None, False, False + ) + print(repr(segments)) + expected = [ + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("red"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + Segment("━", Style.parse("yellow"), False), + ] + assert segments == expected + + +if __name__ == "__main__": + bar = ProgressBar(completed=11, width=50) + bar_render = render(bar) + print(repr(bar_render)) + bar.update(completed=12) + bar_render = render(bar) + print(repr(bar_render)) diff --git a/tests/test_block_bar.py b/tests/test_block_bar.py new file mode 100644 index 0000000..973b9e8 --- /dev/null +++ b/tests/test_block_bar.py @@ -0,0 +1,53 @@ +from rich.bar import Bar + +from .render import render + + +expected = [ + "\x1b[39;49m ▐█████████████████████████ \x1b[0m\n", + "\x1b[39;49m ██████████████████████▌ \x1b[0m\n", + "\x1b[39;49m \x1b[0m\n", +] + + +def test_repr(): + bar = Bar(size=100, begin=11, end=62, width=50) + assert repr(bar) == "Bar(100, 11, 62)" + + +def test_render(): + bar = Bar(size=100, begin=11, end=62, width=50) + bar_render = render(bar) + assert bar_render == expected[0] + bar = Bar(size=100, begin=12, end=57, width=50) + bar_render = render(bar) + assert bar_render == expected[1] + # begin after end + bar = Bar(size=100, begin=60, end=40, width=50) + bar_render = render(bar) + assert bar_render == expected[2] + + +def test_measure(): + bar = Bar(size=100, begin=11, end=62) + measurement = bar.__rich_measure__(None, 120) + assert measurement.minimum == 4 + assert measurement.maximum == 120 + + +def test_zero_total(): + # Shouldn't throw zero division error + bar = Bar(size=0, begin=0, end=0) + render(bar) + + +if __name__ == "__main__": + bar = Bar(size=100, begin=11, end=62, width=50) + bar_render = render(bar) + print(repr(bar_render)) + bar = Bar(size=100, begin=12, end=57, width=50) + bar_render = render(bar) + print(repr(bar_render)) + bar = Bar(size=100, begin=60, end=40, width=50) + bar_render = render(bar) + print(repr(bar_render)) diff --git a/tests/test_box.py b/tests/test_box.py new file mode 100644 index 0000000..f235a82 --- /dev/null +++ b/tests/test_box.py @@ -0,0 +1,54 @@ +import pytest + +from rich.console import ConsoleOptions, ConsoleDimensions +from rich.box import ASCII, DOUBLE, ROUNDED, HEAVY, SQUARE + + +def test_str(): + assert str(ASCII) == "+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+\n" + + +def test_repr(): + assert repr(ASCII) == "Box(...)" + + +def test_get_top(): + top = HEAVY.get_top(widths=[1, 2]) + assert top == "┏━┳━━┓" + + +def test_get_row(): + head_row = DOUBLE.get_row(widths=[3, 2, 1], level="head") + assert head_row == "╠═══╬══╬═╣" + + row = ASCII.get_row(widths=[1, 2, 3], level="row") + assert row == "|-+--+---|" + + foot_row = ROUNDED.get_row(widths=[2, 1, 3], level="foot") + assert foot_row == "├──┼─┼───┤" + + with pytest.raises(ValueError): + ROUNDED.get_row(widths=[1, 2, 3], level="FOO") + + +def test_get_bottom(): + bottom = HEAVY.get_bottom(widths=[1, 2, 3]) + assert bottom == "┗━┻━━┻━━━┛" + + +def test_box_substitute(): + options = ConsoleOptions( + ConsoleDimensions(80, 25), + legacy_windows=True, + min_width=1, + max_width=100, + is_terminal=True, + encoding="utf-8", + ) + assert HEAVY.substitute(options) == SQUARE + + options.legacy_windows = False + assert HEAVY.substitute(options) == HEAVY + + options.encoding = "ascii" + assert HEAVY.substitute(options) == ASCII diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 0000000..9f167b9 --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,40 @@ +import io +import re + +from rich.console import Console, RenderableType +from rich.__main__ import make_test_card + +from ._card_render import expected + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +def render(renderable: RenderableType) -> str: + console = Console( + width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False + ) + console.print(renderable) + output = replace_link_ids(console.file.getvalue()) + return output + + +def test_card_render(): + card = make_test_card() + result = render(card) + assert result == expected + + +if __name__ == "__main__": + card = make_test_card() + with open("_card_render.py", "wt") as fh: + card_render = render(card) + print(card_render) + fh.write(f"expected={card_render!r}") diff --git a/tests/test_cells.py b/tests/test_cells.py new file mode 100644 index 0000000..06de437 --- /dev/null +++ b/tests/test_cells.py @@ -0,0 +1,11 @@ +from rich import cells + + +def test_set_cell_size(): + assert cells.set_cell_size("foo", 2) == "fo" + assert cells.set_cell_size("foo", 3) == "foo" + assert cells.set_cell_size("foo", 4) == "foo " + assert cells.set_cell_size("😽😽", 4) == "😽😽" + assert cells.set_cell_size("😽😽", 3) == "😽 " + assert cells.set_cell_size("😽😽", 2) == "😽" + assert cells.set_cell_size("😽😽", 1) == " " diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000..49f344e --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,184 @@ +from rich.color import ( + blend_rgb, + parse_rgb_hex, + Color, + ColorParseError, + ColorSystem, + ColorType, + ColorTriplet, +) +from rich.style import Style +from rich.text import Text, Span + +import pytest + + +def test_str() -> None: + assert str(Color.parse("red")) == "<color 'red' 1 (standard)>" + + +def test_repr() -> None: + assert repr(Color.parse("red")) == "<color 'red' 1 (standard)>" + + +def test_rich() -> None: + color = Color.parse("red") + as_text = color.__rich__() + print(repr(as_text)) + print(repr(as_text.spans)) + assert as_text == Text( + "<color 'red' (standard)⬤ >", spans=[Span(23, 24, Style(color=color))] + ) + + +def test_system() -> None: + assert Color.parse("default").system == ColorSystem.STANDARD + assert Color.parse("red").system == ColorSystem.STANDARD + assert Color.parse("#ff0000").system == ColorSystem.TRUECOLOR + + +def test_windows() -> None: + assert Color("red", ColorType.WINDOWS, number=1).get_ansi_codes() == ("31",) + + +def test_truecolor() -> None: + assert Color.parse("#ff0000").get_truecolor() == ColorTriplet(255, 0, 0) + assert Color.parse("red").get_truecolor() == ColorTriplet(128, 0, 0) + assert Color.parse("color(1)").get_truecolor() == ColorTriplet(128, 0, 0) + assert Color.parse("color(17)").get_truecolor() == ColorTriplet(0, 0, 95) + assert Color.parse("default").get_truecolor() == ColorTriplet(0, 0, 0) + assert Color.parse("default").get_truecolor(foreground=False) == ColorTriplet( + 255, 255, 255 + ) + assert Color("red", ColorType.WINDOWS, number=1).get_truecolor() == ColorTriplet( + 197, 15, 31 + ) + + +def test_parse_success() -> None: + assert Color.parse("default") == Color("default", ColorType.DEFAULT, None, None) + assert Color.parse("red") == Color("red", ColorType.STANDARD, 1, None) + assert Color.parse("bright_red") == Color("bright_red", ColorType.STANDARD, 9, None) + assert Color.parse("yellow4") == Color("yellow4", ColorType.EIGHT_BIT, 106, None) + assert Color.parse("color(100)") == Color( + "color(100)", ColorType.EIGHT_BIT, 100, None + ) + assert Color.parse("#112233") == Color( + "#112233", ColorType.TRUECOLOR, None, ColorTriplet(0x11, 0x22, 0x33) + ) + assert Color.parse("rgb(90,100,110)") == Color( + "rgb(90,100,110)", ColorType.TRUECOLOR, None, ColorTriplet(90, 100, 110) + ) + + +def test_from_triplet() -> None: + assert Color.from_triplet(ColorTriplet(0x10, 0x20, 0x30)) == Color( + "#102030", ColorType.TRUECOLOR, None, ColorTriplet(0x10, 0x20, 0x30) + ) + + +def test_from_rgb() -> None: + assert Color.from_rgb(0x10, 0x20, 0x30) == Color( + "#102030", ColorType.TRUECOLOR, None, ColorTriplet(0x10, 0x20, 0x30) + ) + + +def test_from_ansi() -> None: + assert Color.from_ansi(1) == Color("color(1)", ColorType.STANDARD, 1) + + +def test_default() -> None: + assert Color.default() == Color("default", ColorType.DEFAULT, None, None) + + +def test_parse_error() -> None: + with pytest.raises(ColorParseError): + Color.parse("256") + with pytest.raises(ColorParseError): + Color.parse("color(256)") + with pytest.raises(ColorParseError): + Color.parse("rgb(999,0,0)") + with pytest.raises(ColorParseError): + Color.parse("rgb(0,0)") + with pytest.raises(ColorParseError): + Color.parse("rgb(0,0,0,0)") + with pytest.raises(ColorParseError): + Color.parse("nosuchcolor") + with pytest.raises(ColorParseError): + Color.parse("#xxyyzz") + + +def test_get_ansi_codes() -> None: + assert Color.parse("default").get_ansi_codes() == ("39",) + assert Color.parse("default").get_ansi_codes(False) == ("49",) + assert Color.parse("red").get_ansi_codes() == ("31",) + assert Color.parse("red").get_ansi_codes(False) == ("41",) + assert Color.parse("color(1)").get_ansi_codes() == ("31",) + assert Color.parse("color(1)").get_ansi_codes(False) == ("41",) + assert Color.parse("#ff0000").get_ansi_codes() == ("38", "2", "255", "0", "0") + assert Color.parse("#ff0000").get_ansi_codes(False) == ("48", "2", "255", "0", "0") + + +def test_downgrade() -> None: + + assert Color.parse("color(9)").downgrade(0) == Color( + "color(9)", ColorType.STANDARD, 9, None + ) + + assert Color.parse("#000000").downgrade(ColorSystem.EIGHT_BIT) == Color( + "#000000", ColorType.EIGHT_BIT, 16, None + ) + + assert Color.parse("#ffffff").downgrade(ColorSystem.EIGHT_BIT) == Color( + "#ffffff", ColorType.EIGHT_BIT, 231, None + ) + + assert Color.parse("#404142").downgrade(ColorSystem.EIGHT_BIT) == Color( + "#404142", ColorType.EIGHT_BIT, 237, None + ) + + assert Color.parse("#ff0000").downgrade(ColorSystem.EIGHT_BIT) == Color( + "#ff0000", ColorType.EIGHT_BIT, 196, None + ) + + assert Color.parse("#ff0000").downgrade(ColorSystem.STANDARD) == Color( + "#ff0000", ColorType.STANDARD, 1, None + ) + + assert Color.parse("color(9)").downgrade(ColorSystem.STANDARD) == Color( + "color(9)", ColorType.STANDARD, 9, None + ) + + assert Color.parse("color(20)").downgrade(ColorSystem.STANDARD) == Color( + "color(20)", ColorType.STANDARD, 4, None + ) + + assert Color.parse("red").downgrade(ColorSystem.WINDOWS) == Color( + "red", ColorType.WINDOWS, 1, None + ) + + assert Color.parse("bright_red").downgrade(ColorSystem.WINDOWS) == Color( + "bright_red", ColorType.WINDOWS, 9, None + ) + + assert Color.parse("#ff0000").downgrade(ColorSystem.WINDOWS) == Color( + "#ff0000", ColorType.WINDOWS, 1, None + ) + + assert Color.parse("color(255)").downgrade(ColorSystem.WINDOWS) == Color( + "color(255)", ColorType.WINDOWS, 15, None + ) + + assert Color.parse("#00ff00").downgrade(ColorSystem.STANDARD) == Color( + "#00ff00", ColorType.STANDARD, 2, None + ) + + +def test_parse_rgb_hex() -> None: + assert parse_rgb_hex("aabbcc") == ColorTriplet(0xAA, 0xBB, 0xCC) + + +def test_blend_rgb() -> None: + assert blend_rgb( + ColorTriplet(10, 20, 30), ColorTriplet(30, 40, 50) + ) == ColorTriplet(20, 30, 40) diff --git a/tests/test_color_triplet.py b/tests/test_color_triplet.py new file mode 100644 index 0000000..4a592c8 --- /dev/null +++ b/tests/test_color_triplet.py @@ -0,0 +1,16 @@ +from rich.color_triplet import ColorTriplet + + +def test_hex(): + assert ColorTriplet(255, 255, 255).hex == "#ffffff" + assert ColorTriplet(0, 255, 0).hex == "#00ff00" + + +def test_rgb(): + assert ColorTriplet(255, 255, 255).rgb == "rgb(255,255,255)" + assert ColorTriplet(0, 255, 0).rgb == "rgb(0,255,0)" + + +def test_normalized(): + assert ColorTriplet(255, 255, 255).normalized == (1.0, 1.0, 1.0) + assert ColorTriplet(0, 255, 0).normalized == (0.0, 1.0, 0.0) diff --git a/tests/test_columns.py b/tests/test_columns.py new file mode 100644 index 0000000..927aba2 --- /dev/null +++ b/tests/test_columns.py @@ -0,0 +1,72 @@ +# encoding=utf-8 + +import io + +from rich.columns import Columns +from rich.console import Console + +COLUMN_DATA = [ + "Ursus americanus", + "American buffalo", + "Bison bison", + "American crow", + "Corvus brachyrhynchos", + "American marten", + "Martes americana", + "American racer", + "Coluber constrictor", + "American woodcock", + "Scolopax minor", + "Anaconda (unidentified)", + "Eunectes sp.", + "Andean goose", + "Chloephaga melanoptera", + "Ant", + "Anteater, australian spiny", + "Tachyglossus aculeatus", + "Anteater, giant", +] + + +def render(): + console = Console(file=io.StringIO(), width=100, legacy_windows=False) + + console.rule("empty") + empty_columns = Columns([]) + console.print(empty_columns) + columns = Columns(COLUMN_DATA) + columns.add_renderable("Myrmecophaga tridactyla") + console.rule("optimal") + console.print(columns) + console.rule("optimal, expand") + columns.expand = True + console.print(columns) + console.rule("columm first, optimal") + columns.column_first = True + columns.expand = False + console.print(columns) + console.rule("column first, right to left") + columns.right_to_left = True + console.print(columns) + console.rule("equal columns, expand") + columns.equal = True + columns.expand = True + console.print(columns) + console.rule("fixed width") + columns.width = 16 + columns.expand = False + console.print(columns) + console.print() + render_result = console.file.getvalue() + return render_result + + +def test_render(): + expected = "────────────────────────────────────────────── empty ───────────────────────────────────────────────\n───────────────────────────────────────────── optimal ──────────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n───────────────────────────────────────── optimal, expand ──────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n────────────────────────────────────── columm first, optimal ───────────────────────────────────────\nUrsus americanus American marten Scolopax minor Ant \nAmerican buffalo Martes americana Anaconda (unidentified) Anteater, australian spiny\nBison bison American racer Eunectes sp. Tachyglossus aculeatus \nAmerican crow Coluber constrictor Andean goose Anteater, giant \nCorvus brachyrhynchos American woodcock Chloephaga melanoptera Myrmecophaga tridactyla \n─────────────────────────────────── column first, right to left ────────────────────────────────────\nAnt Scolopax minor American marten Ursus americanus \nAnteater, australian spiny Anaconda (unidentified) Martes americana American buffalo \nTachyglossus aculeatus Eunectes sp. American racer Bison bison \nAnteater, giant Andean goose Coluber constrictor American crow \nMyrmecophaga tridactyla Chloephaga melanoptera American woodcock Corvus brachyrhynchos\n────────────────────────────────────── equal columns, expand ───────────────────────────────────────\nChloephaga melanoptera American racer Ursus americanus \nAnt Coluber constrictor American buffalo \nAnteater, australian spiny American woodcock Bison bison \nTachyglossus aculeatus Scolopax minor American crow \nAnteater, giant Anaconda (unidentified) Corvus brachyrhynchos \nMyrmecophaga tridactyla Eunectes sp. American marten \n Andean goose Martes americana \n─────────────────────────────────────────── fixed width ────────────────────────────────────────────\nAnteater, Eunectes sp. Coluber Corvus Ursus americanus \naustralian spiny constrictor brachyrhynchos \nTachyglossus Andean goose American American marten American buffalo \naculeatus woodcock \nAnteater, giant Chloephaga Scolopax minor Martes americana Bison bison \n melanoptera \nMyrmecophaga Ant Anaconda American racer American crow \ntridactyla (unidentified) \n\n" + assert render() == expected + + +if __name__ == "__main__": + result = render() + print(result) + print(repr(result)) diff --git a/tests/test_columns_align.py b/tests/test_columns_align.py new file mode 100644 index 0000000..456510a --- /dev/null +++ b/tests/test_columns_align.py @@ -0,0 +1,43 @@ +# encoding=utf-8 + +import io + +from rich import box +from rich.columns import Columns +from rich.console import Console +from rich.panel import Panel + + +def render(): + console = Console(file=io.StringIO(), width=100, legacy_windows=False) + panel = Panel.fit("foo", box=box.SQUARE, padding=0) + columns = Columns([panel] * 4) + columns.expand = True + console.rule("no align") + console.print(columns) + + columns.align = "left" + console.rule("left align") + console.print(columns) + + columns.align = "center" + console.rule("center align") + console.print(columns) + + columns.align = "right" + console.rule("right align") + console.print(columns) + + return console.file.getvalue() + + +def test_align(): + result = render() + expected = "───────────────────────────────────────────── no align ─────────────────────────────────────────────\n┌───┐ ┌───┐ ┌───┐ ┌───┐ \n│foo│ │foo│ │foo│ │foo│ \n└───┘ └───┘ └───┘ └───┘ \n──────────────────────────────────────────── left align ────────────────────────────────────────────\n┌───┐ ┌───┐ ┌───┐ ┌───┐ \n│foo│ │foo│ │foo│ │foo│ \n└───┘ └───┘ └───┘ └───┘ \n─────────────────────────────────────────── center align ───────────────────────────────────────────\n ┌───┐ ┌───┐ ┌───┐ ┌───┐ \n │foo│ │foo│ │foo│ │foo│ \n └───┘ └───┘ └───┘ └───┘ \n─────────────────────────────────────────── right align ────────────────────────────────────────────\n ┌───┐ ┌───┐ ┌───┐ ┌───┐\n │foo│ │foo│ │foo│ │foo│\n └───┘ └───┘ └───┘ └───┘\n" + assert result == expected + + +if __name__ == "__main__": + rendered = render() + print(rendered) + print(repr(rendered)) diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100644 index 0000000..ad1e0f1 --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,545 @@ +import datetime +import io +import os +import sys +import tempfile +from typing import Optional + +import pytest + +from rich import errors +from rich.color import ColorSystem +from rich.console import ( + CaptureError, + Console, + ConsoleDimensions, + ConsoleOptions, + render_group, +) +from rich.measure import measure_renderables +from rich.pager import SystemPager +from rich.panel import Panel +from rich.status import Status +from rich.style import Style +from rich.text import Text + + +def test_dumb_terminal(): + console = Console(force_terminal=True) + assert console.color_system is not None + + console = Console(force_terminal=True, _environ={"TERM": "dumb"}) + assert console.color_system is None + width, height = console.size + assert width == 80 + assert height == 25 + + +def test_soft_wrap(): + console = Console(file=io.StringIO(), width=20, soft_wrap=True) + console.print("foo " * 10) + assert console.file.getvalue() == "foo " * 20 + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") +def test_16color_terminal(): + console = Console( + force_terminal=True, _environ={"TERM": "xterm-16color"}, legacy_windows=False + ) + assert console.color_system == "standard" + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") +def test_truecolor_terminal(): + console = Console( + force_terminal=True, + legacy_windows=False, + _environ={"COLORTERM": "truecolor", "TERM": "xterm-16color"}, + ) + assert console.color_system == "truecolor" + + +def test_console_options_update(): + options = ConsoleOptions( + ConsoleDimensions(80, 25), + legacy_windows=False, + min_width=10, + max_width=20, + is_terminal=False, + encoding="utf-8", + ) + options1 = options.update(width=15) + assert options1.min_width == 15 and options1.max_width == 15 + + options2 = options.update(min_width=5, max_width=15, justify="right") + assert ( + options2.min_width == 5 + and options2.max_width == 15 + and options2.justify == "right" + ) + + options_copy = options.update() + assert options_copy == options and options_copy is not options + + +def test_init(): + console = Console(color_system=None) + assert console._color_system == None + console = Console(color_system="standard") + assert console._color_system == ColorSystem.STANDARD + console = Console(color_system="auto") + + +def test_size(): + console = Console() + w, h = console.size + assert console.width == w + + console = Console(width=99, height=101) + w, h = console.size + assert w == 99 and h == 101 + + +def test_repr(): + console = Console() + assert isinstance(repr(console), str) + assert isinstance(str(console), str) + + +def test_print(): + console = Console(file=io.StringIO(), color_system="truecolor") + console.print("foo") + assert console.file.getvalue() == "foo\n" + + +def test_log(): + console = Console( + file=io.StringIO(), + width=80, + color_system="truecolor", + log_time_format="TIME", + log_path=False, + ) + console.log("foo", style="red") + expected = "\x1b[2;36mTIME\x1b[0m\x1b[2;36m \x1b[0m\x1b[31mfoo\x1b[0m\x1b[31m \x1b[0m\n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_log_milliseconds(): + def time_formatter(timestamp: datetime) -> Text: + return Text("TIME") + + console = Console( + file=io.StringIO(), width=40, log_time_format=time_formatter, log_path=False + ) + console.log("foo") + result = console.file.getvalue() + assert result == "TIME foo \n" + + +def test_print_empty(): + console = Console(file=io.StringIO(), color_system="truecolor") + console.print() + assert console.file.getvalue() == "\n" + + +def test_markup_highlight(): + console = Console(file=io.StringIO(), color_system="truecolor") + console.print("'[bold]foo[/bold]'") + assert ( + console.file.getvalue() + == "\x1b[32m'\x1b[0m\x1b[1;32mfoo\x1b[0m\x1b[32m'\x1b[0m\n" + ) + + +def test_print_style(): + console = Console(file=io.StringIO(), color_system="truecolor") + console.print("foo", style="bold") + assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m\n" + + +def test_show_cursor(): + console = Console(file=io.StringIO(), force_terminal=True, legacy_windows=False) + console.show_cursor(False) + console.print("foo") + console.show_cursor(True) + assert console.file.getvalue() == "\x1b[?25lfoo\n\x1b[?25h" + + +def test_clear(): + console = Console(file=io.StringIO(), force_terminal=True) + console.clear() + console.clear(home=False) + assert console.file.getvalue() == "\033[2J\033[H" + "\033[2J" + + +def test_clear_no_terminal(): + console = Console(file=io.StringIO()) + console.clear() + console.clear(home=False) + assert console.file.getvalue() == "" + + +def test_get_style(): + console = Console() + console.get_style("repr.brace") == Style(bold=True) + + +def test_get_style_default(): + console = Console() + console.get_style("foobar", default="red") == Style(color="red") + + +def test_get_style_error(): + console = Console() + with pytest.raises(errors.MissingStyle): + console.get_style("nosuchstyle") + with pytest.raises(errors.MissingStyle): + console.get_style("foo bar") + + +def test_render_error(): + console = Console() + with pytest.raises(errors.NotRenderableError): + list(console.render([], console.options)) + + +def test_control(): + console = Console(file=io.StringIO(), force_terminal=True) + console.control("FOO") + console.print("BAR") + assert console.file.getvalue() == "FOOBAR\n" + + +def test_capture(): + console = Console() + with console.capture() as capture: + with pytest.raises(CaptureError): + capture.get() + console.print("Hello") + assert capture.get() == "Hello\n" + + +def test_input(monkeypatch, capsys): + def fake_input(prompt): + console.file.write(prompt) + return "bar" + + monkeypatch.setattr("builtins.input", fake_input) + console = Console() + user_input = console.input(prompt="foo:") + assert capsys.readouterr().out == "foo:" + assert user_input == "bar" + + +def test_input_legacy_windows(monkeypatch, capsys): + def fake_input(prompt): + console.file.write(prompt) + return "bar" + + monkeypatch.setattr("builtins.input", fake_input) + console = Console(legacy_windows=True) + user_input = console.input(prompt="foo:") + assert capsys.readouterr().out == "foo:" + assert user_input == "bar" + + +def test_input_password(monkeypatch, capsys): + def fake_input(prompt, stream=None): + console.file.write(prompt) + return "bar" + + import rich.console + + monkeypatch.setattr(rich.console, "getpass", fake_input) + console = Console() + user_input = console.input(prompt="foo:", password=True) + assert capsys.readouterr().out == "foo:" + assert user_input == "bar" + + +def test_status(): + console = Console(file=io.StringIO(), force_terminal=True, width=20) + status = console.status("foo") + assert isinstance(status, Status) + + +def test_justify_none(): + console = Console(file=io.StringIO(), force_terminal=True, width=20) + console.print("FOO", justify=None) + assert console.file.getvalue() == "FOO\n" + + +def test_justify_left(): + console = Console(file=io.StringIO(), force_terminal=True, width=20) + console.print("FOO", justify="left") + assert console.file.getvalue() == "FOO \n" + + +def test_justify_center(): + console = Console(file=io.StringIO(), force_terminal=True, width=20) + console.print("FOO", justify="center") + assert console.file.getvalue() == " FOO \n" + + +def test_justify_right(): + console = Console(file=io.StringIO(), force_terminal=True, width=20) + console.print("FOO", justify="right") + assert console.file.getvalue() == " FOO\n" + + +def test_justify_renderable_none(): + console = Console( + file=io.StringIO(), force_terminal=True, width=20, legacy_windows=False + ) + console.print(Panel("FOO", expand=False, padding=0), justify=None) + assert console.file.getvalue() == "╭───╮\n│FOO│\n╰───╯\n" + + +def test_justify_renderable_left(): + console = Console( + file=io.StringIO(), force_terminal=True, width=10, legacy_windows=False + ) + console.print(Panel("FOO", expand=False, padding=0), justify="left") + assert console.file.getvalue() == "╭───╮ \n│FOO│ \n╰───╯ \n" + + +def test_justify_renderable_center(): + console = Console( + file=io.StringIO(), force_terminal=True, width=10, legacy_windows=False + ) + console.print(Panel("FOO", expand=False, padding=0), justify="center") + assert console.file.getvalue() == " ╭───╮ \n │FOO│ \n ╰───╯ \n" + + +def test_justify_renderable_right(): + console = Console( + file=io.StringIO(), force_terminal=True, width=20, legacy_windows=False + ) + console.print(Panel("FOO", expand=False, padding=0), justify="right") + assert ( + console.file.getvalue() + == " ╭───╮\n │FOO│\n ╰───╯\n" + ) + + +class BrokenRenderable: + def __rich_console__(self, console, options): + pass + + +def test_render_broken_renderable(): + console = Console() + broken = BrokenRenderable() + with pytest.raises(errors.NotRenderableError): + list(console.render(broken, console.options)) + + +def test_export_text(): + console = Console(record=True, width=100) + console.print("[b]foo") + text = console.export_text() + expected = "foo\n" + assert text == expected + + +def test_export_html(): + console = Console(record=True, width=100) + console.print("[b]foo [link=https://example.org]Click[/link]") + html = console.export_html() + expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n.r1 {font-weight: bold}\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span class="r1">foo </span><a href="https://example.org"><span class="r1">Click</span></a>\n</pre>\n </code>\n</body>\n</html>\n' + assert html == expected + + +def test_export_html_inline(): + console = Console(record=True, width=100) + console.print("[b]foo [link=https://example.org]Click[/link]") + html = console.export_html(inline_styles=True) + expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="font-weight: bold">foo </span><a href="https://example.org"><span style="font-weight: bold">Click</span></a>\n</pre>\n </code>\n</body>\n</html>\n' + assert html == expected + + +def test_save_text(): + console = Console(record=True, width=100) + console.print("foo") + with tempfile.TemporaryDirectory() as path: + export_path = os.path.join(path, "rich.txt") + console.save_text(export_path) + with open(export_path, "rt") as text_file: + assert text_file.read() == "foo\n" + + +def test_save_html(): + expected = "<!DOCTYPE html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style=\"font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">foo\n</pre>\n </code>\n</body>\n</html>\n" + console = Console(record=True, width=100) + console.print("foo") + with tempfile.TemporaryDirectory() as path: + export_path = os.path.join(path, "example.html") + console.save_html(export_path) + with open(export_path, "rt") as html_file: + assert html_file.read() == expected + + +def test_no_wrap(): + console = Console(width=10, file=io.StringIO()) + console.print("foo bar baz egg", no_wrap=True) + assert console.file.getvalue() == "foo bar ba\n" + + +def test_soft_wrap(): + console = Console(width=10, file=io.StringIO()) + console.print("foo bar baz egg", soft_wrap=True) + assert console.file.getvalue() == "foo bar baz egg\n" + + +def test_unicode_error() -> None: + try: + with tempfile.TemporaryFile("wt", encoding="ascii") as tmpfile: + console = Console(file=tmpfile) + console.print(":vampire:") + except UnicodeEncodeError as error: + assert "PYTHONIOENCODING" in str(error) + else: + assert False, "didn't raise UnicodeEncodeError" + + +def test_bell() -> None: + console = Console(force_terminal=True) + console.begin_capture() + console.bell() + assert console.end_capture() == "\x07" + + +def test_pager() -> None: + console = Console() + + pager_content: Optional[str] = None + + def mock_pager(content: str) -> None: + nonlocal pager_content + pager_content = content + + pager = SystemPager() + pager._pager = mock_pager + + with console.pager(pager): + console.print("[bold]Hello World") + assert pager_content == "Hello World\n" + + with console.pager(pager, styles=True, links=False): + console.print("[bold link https:/example.org]Hello World") + + assert pager_content == "Hello World\n" + + +def test_out() -> None: + console = Console(width=10) + console.begin_capture() + console.out(*(["foo bar"] * 5), sep=".", end="X") + assert console.end_capture() == "foo bar.foo bar.foo bar.foo bar.foo barX" + + +def test_render_group() -> None: + @render_group(fit=False) + def renderable(): + yield "one" + yield "two" + yield "three" # <- largest width of 5 + yield "four" + + renderables = [renderable() for _ in range(4)] + console = Console(width=42) + min_width, _ = measure_renderables(console, renderables, 42) + assert min_width == 42 + + +def test_render_group_fit() -> None: + @render_group() + def renderable(): + yield "one" + yield "two" + yield "three" # <- largest width of 5 + yield "four" + + renderables = [renderable() for _ in range(4)] + + console = Console(width=42) + + min_width, _ = measure_renderables(console, renderables, 42) + assert min_width == 5 + + +def test_get_time() -> None: + console = Console( + get_time=lambda: 99, get_datetime=lambda: datetime.datetime(1974, 7, 5) + ) + assert console.get_time() == 99 + assert console.get_datetime() == datetime.datetime(1974, 7, 5) + + +def test_console_style() -> None: + console = Console( + file=io.StringIO(), color_system="truecolor", force_terminal=True, style="red" + ) + console.print("foo") + expected = "\x1b[31mfoo\x1b[0m\n" + result = console.file.getvalue() + assert result == expected + + +def test_no_color(): + console = Console( + file=io.StringIO(), color_system="truecolor", force_terminal=True, no_color=True + ) + console.print("[bold magenta on red]FOO") + expected = "\x1b[1mFOO\x1b[0m\n" + result = console.file.getvalue() + print(repr(result)) + assert result == expected + + +def test_quiet(): + console = Console(file=io.StringIO(), quiet=True) + console.print("Hello, World!") + assert console.file.getvalue() == "" + + +def test_no_nested_live(): + console = Console() + with pytest.raises(errors.LiveError): + with console.status("foo"): + with console.status("bar"): + pass + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") +def test_screen(): + console = Console(color_system=None, force_terminal=True, force_interactive=True) + with console.capture() as capture: + with console.screen(): + console.print("Don't panic") + expected = "\x1b[?1049h\x1b[H\x1b[?25lDon't panic\n\x1b[?1049l\x1b[?25h" + result = capture.get() + print(repr(result)) + assert result == expected + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") +def test_screen_update(): + console = Console(width=20, height=4, color_system="truecolor", force_terminal=True) + with console.capture() as capture: + with console.screen() as screen: + screen.update("foo", style="blue") + screen.update("bar") + screen.update() + result = capture.get() + print(repr(result)) + expected = "\x1b[?1049h\x1b[H\x1b[?25l\x1b[34mfoo\x1b[0m\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\x1b[34mbar\x1b[0m\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\x1b[34mbar\x1b[0m\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\n\x1b[34m \x1b[0m\x1b[?1049l\x1b[?25h" + assert result == expected + + +def test_height(): + console = Console(width=80, height=46) + assert console.height == 46 diff --git a/tests/test_constrain.py b/tests/test_constrain.py new file mode 100644 index 0000000..6fc9636 --- /dev/null +++ b/tests/test_constrain.py @@ -0,0 +1,11 @@ +from rich.console import Console +from rich.constrain import Constrain +from rich.text import Text + + +def test_width_of_none(): + console = Console() + constrain = Constrain(Text("foo"), width=None) + min_width, max_width = constrain.__rich_measure__(console, 80) + assert min_width == 3 + assert max_width == 3 diff --git a/tests/test_containers.py b/tests/test_containers.py new file mode 100644 index 0000000..8009898 --- /dev/null +++ b/tests/test_containers.py @@ -0,0 +1,56 @@ +from rich.console import Console +from rich.containers import Lines, Renderables +from rich.text import Span, Text +from rich.style import Style + + +def test_renderables_measure(): + console = Console() + text = Text("foo") + renderables = Renderables([text]) + + result = renderables.__rich_measure__(console, console.width) + _min, _max = result + assert _min == 3 + assert _max == 3 + + assert list(renderables) == [text] + + +def test_renderables_empty(): + console = Console() + renderables = Renderables() + + result = renderables.__rich_measure__(console, console.width) + _min, _max = result + assert _min == 1 + assert _max == 1 + + +def test_lines_rich_console(): + console = Console() + lines = Lines([Text("foo")]) + + result = list(lines.__rich_console__(console, console.options)) + assert result == [Text("foo")] + + +def test_lines_justify(): + console = Console() + lines1 = Lines([Text("foo"), Text("test")]) + lines1.justify(console, 10, justify="left") + assert lines1._lines == [Text("foo "), Text("test ")] + lines1.justify(console, 10, justify="center") + assert lines1._lines == [Text(" foo "), Text(" test ")] + lines1.justify(console, 10, justify="right") + assert lines1._lines == [Text(" foo"), Text(" test")] + + lines2 = Lines([Text("foo bar"), Text("test")]) + lines2.justify(console, 7, justify="full") + assert lines2._lines == [ + Text( + "foo bar", + spans=[Span(0, 3, ""), Span(3, 4, Style.parse("none")), Span(4, 7, "")], + ), + Text("test"), + ] diff --git a/tests/test_control.py b/tests/test_control.py new file mode 100644 index 0000000..d568355 --- /dev/null +++ b/tests/test_control.py @@ -0,0 +1,12 @@ +from rich.control import Control, strip_control_codes + + +def test_control(): + control = Control("FOO") + assert str(control) == "FOO" + + +def test_strip_control_codes(): + assert strip_control_codes("") == "" + assert strip_control_codes("foo\rbar") == "foobar" + assert strip_control_codes("Fear is the mind killer") == "Fear is the mind killer" diff --git a/tests/test_emoji.py b/tests/test_emoji.py new file mode 100644 index 0000000..cc519da --- /dev/null +++ b/tests/test_emoji.py @@ -0,0 +1,24 @@ +import pytest + +from rich.emoji import Emoji, NoEmoji + +from .render import render + + +def test_no_emoji(): + with pytest.raises(NoEmoji): + Emoji("ambivalent_bunny") + + +def test_str_repr(): + assert str(Emoji("pile_of_poo")) == "💩" + assert repr(Emoji("pile_of_poo")) == "<emoji 'pile_of_poo'>" + + +def test_replace(): + assert Emoji.replace("my code is :pile_of_poo:") == "my code is 💩" + + +def test_render(): + render_result = render(Emoji("pile_of_poo")) + assert render_result == "💩" diff --git a/tests/test_file_proxy.py b/tests/test_file_proxy.py new file mode 100644 index 0000000..2218d03 --- /dev/null +++ b/tests/test_file_proxy.py @@ -0,0 +1,27 @@ +import io +import sys + +import pytest + +from rich.console import Console +from rich.file_proxy import FileProxy + + +def test_empty_bytes(): + console = Console() + file_proxy = FileProxy(console, sys.stdout) + # File should raise TypeError when writing bytes + with pytest.raises(TypeError): + file_proxy.write(b"") # type: ignore + with pytest.raises(TypeError): + file_proxy.write(b"foo") # type: ignore + + +def test_flush(): + file = io.StringIO() + console = Console(file=file) + file_proxy = FileProxy(console, file) + file_proxy.write("foo") + assert file.getvalue() == "" + file_proxy.flush() + assert file.getvalue() == "foo\n" diff --git a/tests/test_filesize.py b/tests/test_filesize.py new file mode 100644 index 0000000..937ef73 --- /dev/null +++ b/tests/test_filesize.py @@ -0,0 +1,15 @@ +from rich import filesize + + +def test_traditional(): + assert filesize.decimal(0) == "0 bytes" + assert filesize.decimal(1) == "1 byte" + assert filesize.decimal(2) == "2 bytes" + assert filesize.decimal(1000) == "1.0 kB" + assert filesize.decimal(1.5 * 1000 * 1000) == "1.5 MB" + + +def test_pick_unit_and_suffix(): + units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + assert filesize.pick_unit_and_suffix(50, units, 1024) == (1, "bytes") + assert filesize.pick_unit_and_suffix(2048, units, 1024) == (1024, "KB") diff --git a/tests/test_highlighter.py b/tests/test_highlighter.py new file mode 100644 index 0000000..da5fc9b --- /dev/null +++ b/tests/test_highlighter.py @@ -0,0 +1,83 @@ +"""Tests for the higlighter classes.""" +import pytest +from typing import List + +from rich.highlighter import NullHighlighter, ReprHighlighter +from rich.text import Span, Text + + +def test_wrong_type(): + highlighter = NullHighlighter() + with pytest.raises(TypeError): + highlighter([]) + + +highlight_tests = [ + ("", []), + (" ", []), + ( + "<foo>", + [ + Span(0, 1, "repr.tag_start"), + Span(1, 4, "repr.tag_name"), + Span(4, 5, "repr.tag_end"), + ], + ), + ( + "False True None", + [ + Span(0, 5, "repr.bool_false"), + Span(6, 10, "repr.bool_true"), + Span(11, 15, "repr.none"), + ], + ), + ("foo=bar", [Span(0, 3, "repr.attrib_name"), Span(4, 7, "repr.attrib_value")]), + ( + 'foo="bar"', + [ + Span(0, 3, "repr.attrib_name"), + Span(4, 9, "repr.attrib_value"), + Span(4, 9, "repr.str"), + ], + ), + ("( )", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]), + ("[ ]", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]), + ("{ }", [Span(0, 1, "repr.brace"), Span(2, 3, "repr.brace")]), + (" 1 ", [Span(1, 2, "repr.number")]), + (" 1.2 ", [Span(1, 4, "repr.number")]), + (" 0xff ", [Span(1, 5, "repr.number")]), + (" 1e10 ", [Span(1, 5, "repr.number")]), + (" /foo ", [Span(1, 2, "repr.path"), Span(2, 5, "repr.filename")]), + (" /foo/bar.html ", [Span(1, 6, "repr.path"), Span(6, 14, "repr.filename")]), + ("01-23-45-67-89-AB", [Span(0, 17, "repr.eui48")]), # 6x2 hyphen + ("01-23-45-FF-FE-67-89-AB", [Span(0, 23, "repr.eui64")]), # 8x2 hyphen + ("01:23:45:67:89:AB", [Span(0, 17, "repr.ipv6")]), # 6x2 colon + ("01:23:45:FF:FE:67:89:AB", [Span(0, 23, "repr.ipv6")]), # 8x2 colon + ("0123.4567.89AB", [Span(0, 14, "repr.eui48")]), # 3x4 dot + ("0123.45FF.FE67.89AB", [Span(0, 19, "repr.eui64")]), # 4x4 dot + ("ed-ed-ed-ed-ed-ed", [Span(0, 17, "repr.eui48")]), # lowercase + ("ED-ED-ED-ED-ED-ED", [Span(0, 17, "repr.eui48")]), # uppercase + ("Ed-Ed-Ed-Ed-Ed-Ed", [Span(0, 17, "repr.eui48")]), # mixed case + ("0-00-1-01-2-02", [Span(0, 14, "repr.eui48")]), # dropped zero + (" https://example.org ", [Span(1, 20, "repr.url")]), + (" http://example.org ", [Span(1, 19, "repr.url")]), + (" http://example.org/index.html ", [Span(1, 30, "repr.url")]), + ("No place like 127.0.0.1", [Span(14, 23, "repr.ipv4")]), + ("''", [Span(0, 2, "repr.str")]), + ("'hello'", [Span(0, 7, "repr.str")]), + ("'''hello'''", [Span(0, 11, "repr.str")]), + ('""', [Span(0, 2, "repr.str")]), + ('"hello"', [Span(0, 7, "repr.str")]), + ('"""hello"""', [Span(0, 11, "repr.str")]), + ("\\'foo'", []), +] + + +@pytest.mark.parametrize("test, spans", highlight_tests) +def test_highlight_regex(test: str, spans: List[Span]): + """Tests for the regular expressions used in ReprHighlighter.""" + text = Text(test) + highlighter = ReprHighlighter() + highlighter.highlight(text) + print(text.spans) + assert text.spans == spans diff --git a/tests/test_inspect.py b/tests/test_inspect.py new file mode 100644 index 0000000..0ffc313 --- /dev/null +++ b/tests/test_inspect.py @@ -0,0 +1,205 @@ +import io +import sys + +import pytest + +from rich import inspect +from rich.console import Console + + +skip_py36 = pytest.mark.skipif( + sys.version_info.minor == 6 and sys.version_info.major == 3, + reason="rendered differently on py3.6", +) + + +skip_py37 = pytest.mark.skipif( + sys.version_info.minor == 7 and sys.version_info.major == 3, + reason="rendered differently on py3.7", +) + + +def render(obj, methods=False, value=False, width=50) -> str: + console = Console(file=io.StringIO(), width=width, legacy_windows=False) + inspect(obj, console=console, methods=methods, value=value) + return console.file.getvalue() + + +class InspectError(Exception): + def __str__(self) -> str: + return "INSPECT ERROR" + + +class Foo: + """Foo test + + Second line + """ + + def __init__(self, foo: int) -> None: + """constructor docs.""" + self.foo = foo + + @property + def broken(self): + raise InspectError() + + def method(self, a, b) -> str: + """Multi line + + docs. + """ + return "test" + + def __dir__(self): + return ["__init__", "broken", "method"] + + +@skip_py36 +def test_render(): + console = Console(width=100, file=io.StringIO(), legacy_windows=False) + + foo = Foo("hello") + inspect(foo, console=console, all=True, value=False) + result = console.file.getvalue() + print(repr(result)) + expected = "╭────────────── <class 'tests.test_inspect.Foo'> ──────────────╮\n│ Foo test │\n│ │\n│ broken = InspectError() │\n│ __init__ = def __init__(foo: int) -> None: constructor docs. │\n│ method = def method(a, b) -> str: Multi line │\n╰──────────────────────────────────────────────────────────────╯\n" + assert expected == result + + +def test_inspect_text(): + + expected = ( + "╭──────────────── <class 'str'> ─────────────────╮\n" + "│ str(object='') -> str │\n" + "│ str(bytes_or_buffer[, encoding[, errors]]) -> │\n" + "│ str │\n" + "│ │\n" + "│ 33 attribute(s) not shown. Run │\n" + "│ inspect(inspect) for options. │\n" + "╰────────────────────────────────────────────────╯\n" + ) + print(repr(expected)) + assert expected == render("Hello") + + +@skip_py36 +@skip_py37 +def test_inspect_empty_dict(): + + expected = ( + "╭──────────────── <class 'dict'> ────────────────╮\n" + "│ dict() -> new empty dictionary │\n" + "│ dict(mapping) -> new dictionary initialized │\n" + "│ from a mapping object's │\n" + "│ (key, value) pairs │\n" + "│ dict(iterable) -> new dictionary initialized │\n" + "│ as if via: │\n" + "│ d = {} │\n" + "│ for k, v in iterable: │\n" + "│ d[k] = v │\n" + "│ dict(**kwargs) -> new dictionary initialized │\n" + "│ with the name=value pairs │\n" + "│ in the keyword argument list. For │\n" + "│ example: dict(one=1, two=2) │\n" + "│ │\n" + ) + assert render({}).startswith(expected) + + +def test_inspect_builtin_function(): + + expected = ( + "╭────────── <built-in function print> ───────────╮\n" + "│ def print(...) │\n" + "│ │\n" + "│ print(value, ..., sep=' ', end='\\n', │\n" + "│ file=sys.stdout, flush=False) │\n" + "│ │\n" + "│ 29 attribute(s) not shown. Run │\n" + "│ inspect(inspect) for options. │\n" + "╰────────────────────────────────────────────────╯\n" + ) + assert expected == render(print) + + +@skip_py36 +def test_inspect_integer(): + + expected = ( + "╭────── <class 'int'> ───────╮\n" + "│ int([x]) -> integer │\n" + "│ int(x, base=10) -> integer │\n" + "│ │\n" + "│ denominator = 1 │\n" + "│ imag = 0 │\n" + "│ numerator = 1 │\n" + "│ real = 1 │\n" + "╰────────────────────────────╯\n" + ) + assert expected == render(1) + + +@skip_py36 +def test_inspect_integer_with_value(): + + expected = "╭────── <class 'int'> ───────╮\n│ int([x]) -> integer │\n│ int(x, base=10) -> integer │\n│ │\n│ ╭────────────────────────╮ │\n│ │ 1 │ │\n│ ╰────────────────────────╯ │\n│ │\n│ denominator = 1 │\n│ imag = 0 │\n│ numerator = 1 │\n│ real = 1 │\n╰────────────────────────────╯\n" + value = render(1, value=True) + print(repr(value)) + assert expected == value + + +@skip_py36 +@skip_py37 +def test_inspect_integer_with_methods(): + + expected = ( + "╭──────────────── <class 'int'> ─────────────────╮\n" + "│ int([x]) -> integer │\n" + "│ int(x, base=10) -> integer │\n" + "│ │\n" + "│ denominator = 1 │\n" + "│ imag = 0 │\n" + "│ numerator = 1 │\n" + "│ real = 1 │\n" + "│ as_integer_ratio = def as_integer_ratio(): │\n" + "│ Return integer ratio. │\n" + "│ bit_length = def bit_length(): Number of │\n" + "│ bits necessary to represent │\n" + "│ self in binary. │\n" + "│ conjugate = def conjugate(...) Returns │\n" + "│ self, the complex conjugate │\n" + "│ of any int. │\n" + "│ from_bytes = def from_bytes(bytes, │\n" + "│ byteorder, *, │\n" + "│ signed=False): Return the │\n" + "│ integer represented by the │\n" + "│ given array of bytes. │\n" + "│ to_bytes = def to_bytes(length, │\n" + "│ byteorder, *, │\n" + "│ signed=False): Return an │\n" + "│ array of bytes representing │\n" + "│ an integer. │\n" + "╰────────────────────────────────────────────────╯\n" + ) + assert expected == render(1, methods=True) + + +@skip_py36 +@skip_py37 +def test_broken_call_attr(): + class NotCallable: + __call__ = 5 # Passes callable() but isn't really callable + + def __repr__(self): + return "NotCallable()" + + class Foo: + foo = NotCallable() + + foo = Foo() + assert callable(foo.foo) + expected = "╭─ <class 'tests.test_inspect.test_broken_call_attr.<locals>.Foo'> ─╮\n│ foo = NotCallable() │\n╰───────────────────────────────────────────────────────────────────╯\n" + result = render(foo, methods=True, width=100) + print(repr(result)) + assert expected == result diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py new file mode 100644 index 0000000..a69f211 --- /dev/null +++ b/tests/test_jupyter.py @@ -0,0 +1,7 @@ +from rich.console import Console + + +def test_jupyter(): + console = Console(force_jupyter=True) + assert console.width == 93 + assert console.color_system == "truecolor" diff --git a/tests/test_layout.py b/tests/test_layout.py new file mode 100644 index 0000000..08beccc --- /dev/null +++ b/tests/test_layout.py @@ -0,0 +1,54 @@ +import sys +import pytest + +from rich.console import Console +from rich.layout import Layout +from rich.panel import Panel + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") +def test_render(): + layout = Layout(name="root") + repr(layout) + + layout.split(Layout(name="top"), Layout(name="bottom")) + top = layout["top"] + top.update(Panel("foo")) + + print(type(top._renderable)) + assert isinstance(top.renderable, Panel) + layout["bottom"].split( + Layout(name="left"), Layout(name="right"), direction="horizontal" + ) + + assert layout["root"].name == "root" + assert layout["left"].name == "left" + with pytest.raises(KeyError): + top["asdasd"] + + layout["left"].update("foobar") + print(layout["left"].children) + + console = Console(width=60, color_system=None) + + with console.capture() as capture: + console.print(layout, height=10) + + result = capture.get() + expected = "╭──────────────────────────────────────────────────────────╮\n│ foo │\n│ │\n│ │\n╰──────────────────────────────────────────────────────────╯\nfoobar ╭───── 'right' (30 x 5) ─────╮\n │ { │\n │ 'size': None, │\n │ 'minimum_size': 1, │\n ╰────────────────────────────╯\n" + assert result == expected + + +def test_tree(): + layout = Layout(name="root") + layout.split(Layout("foo", size=2), Layout("bar")) + + console = Console(width=60, color_system=None) + + with console.capture() as capture: + console.print(layout.tree, height=10) + result = capture.get() + print(repr(result)) + expected = "⬇ 'root' (ratio=1) \n├── ■ (size=2) \n└── ■ (ratio=1) \n" + + assert result == expected diff --git a/tests/test_live.py b/tests/test_live.py new file mode 100644 index 0000000..bc370a0 --- /dev/null +++ b/tests/test_live.py @@ -0,0 +1,172 @@ +# encoding=utf-8 +import io +import time +from typing import Optional + +# import pytest +from rich.console import Console +from rich.text import Text +from rich.live import Live + + +def create_capture_console( + *, width: int = 60, height: int = 80, force_terminal: Optional[bool] = True +) -> Console: + return Console( + width=width, + height=height, + force_terminal=force_terminal, + legacy_windows=False, + color_system=None, # use no color system to reduce complexity of output + ) + + +def test_live_state() -> None: + + with Live("") as live: + assert live._started + live.start() + + assert live.renderable == "" + + assert live._started + live.stop() + assert not live._started + + assert not live._started + + +def test_growing_display() -> None: + console = create_capture_console() + console.begin_capture() + with Live(console=console, auto_refresh=False) as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + print(repr(output)) + assert ( + output + == "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h" + ) + + +def test_growing_display_transient() -> None: + console = create_capture_console() + console.begin_capture() + with Live(console=console, auto_refresh=False, transient=True) as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K" + ) + + +def test_growing_display_overflow_ellipsis() -> None: + console = create_capture_console(height=5) + console.begin_capture() + with Live( + console=console, auto_refresh=False, vertical_overflow="ellipsis" + ) as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h" + ) + + +def test_growing_display_overflow_crop() -> None: + console = create_capture_console(height=5) + console.begin_capture() + with Live(console=console, auto_refresh=False, vertical_overflow="crop") as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h" + ) + + +def test_growing_display_overflow_visible() -> None: + console = create_capture_console(height=5) + console.begin_capture() + with Live(console=console, auto_refresh=False, vertical_overflow="visible") as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h" + ) + + +def test_growing_display_autorefresh() -> None: + """Test generating a table but using auto-refresh from threading""" + console = create_capture_console() + + console = create_capture_console(height=5) + console.begin_capture() + with Live(console=console, auto_refresh=True, vertical_overflow="visible") as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display) + time.sleep(0.2) + + # no way to truly test w/ multithreading, just make sure it doesn't crash + + +def test_growing_display_console_redirect() -> None: + console = create_capture_console() + console.begin_capture() + with Live(console=console, auto_refresh=False) as live: + display = "" + for step in range(10): + console.print(f"Running step {step}") + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "\x1b[?25lRunning step 0\n\r\x1b[2KStep 0\n\r\x1b[2K\x1b[1A\x1b[2KRunning step 1\nStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 2\nStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 3\nStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 4\nStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 5\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 6\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 7\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 8\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 9\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h" + ) + + +def test_growing_display_file_console() -> None: + console = create_capture_console(force_terminal=False) + console.begin_capture() + with Live(console=console, auto_refresh=False) as live: + display = "" + for step in range(10): + display += f"Step {step}\n" + live.update(display, refresh=True) + output = console.end_capture() + assert ( + output + == "Step 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n" + ) + + +def test_live_screen() -> None: + console = create_capture_console(width=20, height=5) + console.begin_capture() + with Live(Text("foo"), screen=True, console=console, auto_refresh=False) as live: + live.refresh() + result = console.end_capture() + print(repr(result)) + expected = "\x1b[?1049h\x1b[H\x1b[?25l\x1b[Hfoo \n \n \n \n \x1b[Hfoo \n \n \n \n \x1b[?25h\x1b[?1049l" + assert result == expected diff --git a/tests/test_live_render.py b/tests/test_live_render.py new file mode 100644 index 0000000..17b5632 --- /dev/null +++ b/tests/test_live_render.py @@ -0,0 +1,44 @@ +import pytest +from rich.live_render import LiveRender +from rich.console import Console, ConsoleDimensions, ConsoleOptions +from rich.style import Style +from rich.segment import Segment + + +@pytest.fixture +def live_render(): + return LiveRender(renderable="my string") + + +def test_renderable(live_render): + assert live_render.renderable == "my string" + live_render.set_renderable("another string") + assert live_render.renderable == "another string" + + +def test_position_cursor(live_render): + assert str(live_render.position_cursor()) == "" + live_render._shape = (80, 2) + assert str(live_render.position_cursor()) == "\r\x1b[2K\x1b[1A\x1b[2K" + + +def test_restore_cursor(live_render): + assert str(live_render.restore_cursor()) == "" + live_render._shape = (80, 2) + assert str(live_render.restore_cursor()) == "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K" + + +def test_rich_console(live_render): + options = ConsoleOptions( + ConsoleDimensions(80, 25), + legacy_windows=False, + min_width=10, + max_width=20, + is_terminal=False, + encoding="utf-8", + ) + rich_console = live_render.__rich_console__(Console(), options) + assert [Segment("my string", Style.parse("none"))] == list(rich_console) + live_render.style = "red" + rich_console = live_render.__rich_console__(Console(), options) + assert [Segment("my string", Style.parse("red"))] == list(rich_console) diff --git a/tests/test_log.py b/tests/test_log.py new file mode 100644 index 0000000..82dbfe2 --- /dev/null +++ b/tests/test_log.py @@ -0,0 +1,59 @@ +# encoding=utf-8 + + +import io +import re + +from rich.console import Console + + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +test_data = [1, 2, 3] + + +def render_log(): + console = Console( + file=io.StringIO(), + width=80, + force_terminal=True, + log_time_format="[TIME]", + color_system="truecolor", + legacy_windows=False, + ) + console.log() + console.log("Hello from", console, "!") + console.log(test_data, log_locals=True) + return replace_link_ids(console.file.getvalue()) + + +def test_log(): + expected = replace_link_ids( + "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:34\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:35\x1b[0m\n \x1b[34m╭─\x1b[0m\x1b[34m───────────────────── \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m ─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \n \x1b[34m│\x1b[0m \x1b[3;33mconsole\x1b[0m\x1b[31m =\x1b[0m \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m \x1b[34m│\x1b[0m \n \x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \n" + ) + rendered = render_log() + print(repr(rendered)) + assert rendered == expected + + +def test_justify(): + console = Console(width=20, log_path=False, log_time=False, color_system=None) + console.begin_capture() + console.log("foo", justify="right") + result = console.end_capture() + assert result == " foo\n" + + +if __name__ == "__main__": + render = render_log() + print(render) + print(repr(render)) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..566c685 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,80 @@ +import io +import sys +import os +import logging +import pytest + +from rich.console import Console +from rich.logging import RichHandler + +handler = RichHandler( + console=Console( + file=io.StringIO(), force_terminal=True, width=80, color_system="truecolor" + ), + enable_link_path=False, +) +logging.basicConfig( + level="NOTSET", format="%(message)s", datefmt="[DATE]", handlers=[handler] +) +log = logging.getLogger("rich") + + +skip_win = pytest.mark.skipif( + os.name == "nt", + reason="rendered differently on windows", +) + + +@skip_win +def test_exception(): + console = Console( + file=io.StringIO(), force_terminal=True, width=140, color_system="truecolor" + ) + handler_with_tracebacks = RichHandler( + console=console, enable_link_path=False, rich_tracebacks=True + ) + log.addHandler(handler_with_tracebacks) + + try: + 1 / 0 + except ZeroDivisionError: + log.exception("message") + + render = handler_with_tracebacks.console.file.getvalue() + print(render) + + assert "ZeroDivisionError" in render + assert "message" in render + assert "division by zero" in render + + +def test_exception_with_extra_lines(): + console = Console( + file=io.StringIO(), force_terminal=True, width=140, color_system="truecolor" + ) + handler_extra_lines = RichHandler( + console=console, + enable_link_path=False, + markup=True, + rich_tracebacks=True, + tracebacks_extra_lines=5, + ) + log.addHandler(handler_extra_lines) + + try: + 1 / 0 + except ZeroDivisionError: + log.exception("message") + + render = handler_extra_lines.console.file.getvalue() + print(render) + + assert "ZeroDivisionError" in render + assert "message" in render + assert "division by zero" in render + + +if __name__ == "__main__": + render = make_log() + print(render) + print(repr(render)) diff --git a/tests/test_lrucache.py b/tests/test_lrucache.py new file mode 100644 index 0000000..9a1d7d1 --- /dev/null +++ b/tests/test_lrucache.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +import unittest + +from rich._lru_cache import LRUCache + + +def test_lru_cache(): + cache = LRUCache(3) + + # insert some values + cache["foo"] = 1 + cache["bar"] = 2 + cache["baz"] = 3 + assert "foo" in cache + + # Cache size is 3, so the following should kick oldest one out + cache["egg"] = 4 + assert "foo" not in cache + assert "egg" in "egg" in cache + + # cache is now full + # look up two keys + cache["bar"] + cache["baz"] + + # Insert a new value + cache["eggegg"] = 5 + # Check it kicked out the 'oldest' key + assert "egg" not in cache diff --git a/tests/test_markdown.py b/tests/test_markdown.py new file mode 100644 index 0000000..fdd56b7 --- /dev/null +++ b/tests/test_markdown.py @@ -0,0 +1,117 @@ +# coding=utf-8 + +MARKDOWN = """Heading +======= + +Sub-heading +----------- + +### Heading + +#### H4 Heading + +##### H5 Heading + +###### H6 Heading + + +Paragraphs are separated +by a blank line. + +Two spaces at the end of a line +produces a line break. + +Text attributes _italic_, +**bold**, `monospace`. + +Horizontal rule: + +--- + +Bullet list: + + * apples + * oranges + * pears + +Numbered list: + + 1. lather + 2. rinse + 3. repeat + +An [example](http://example.com). + +> Markdown uses email-style > characters for blockquoting. +> +> Lorem ipsum + +![progress](https://github.com/willmcgugan/rich/raw/master/imgs/progress.gif) + + +``` +a=1 +``` + +```python +import this +``` + +```somelang +foobar +``` + +""" + +import io +import re + +from rich.console import Console, RenderableType +from rich.markdown import Markdown + + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +def render(renderable: RenderableType) -> str: + console = Console( + width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False + ) + console.print(renderable) + output = replace_link_ids(console.file.getvalue()) + return output + + +def test_markdown_render(): + markdown = Markdown(MARKDOWN) + rendered_markdown = render(markdown) + expected = "╔══════════════════════════════════════════════════════════════════════════════════════════════════╗\n║ \x1b[1mHeading\x1b[0m ║\n╚══════════════════════════════════════════════════════════════════════════════════════════════════╝\n\n\n \x1b[1;4mSub-heading\x1b[0m \n\n \x1b[1mHeading\x1b[0m \n\n \x1b[1;2mH4 Heading\x1b[0m \n\n \x1b[4mH5 Heading\x1b[0m \n\n \x1b[3mH6 Heading\x1b[0m \n\nParagraphs are separated by a blank line. \n\nTwo spaces at the end of a line \nproduces a line break. \n\nText attributes \x1b[3mitalic\x1b[0m, \x1b[1mbold\x1b[0m, \x1b[97;40mmonospace\x1b[0m. \n\nHorizontal rule: \n\n\x1b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\nBullet list: \n\n\x1b[1;33m • \x1b[0mapples \n\x1b[1;33m • \x1b[0moranges \n\x1b[1;33m • \x1b[0mpears \n\nNumbered list: \n\n\x1b[1;33m 1 \x1b[0mlather \n\x1b[1;33m 2 \x1b[0mrinse \n\x1b[1;33m 3 \x1b[0mrepeat \n\nAn \x1b]8;id=0;foo\x1b\\\x1b[94mexample\x1b[0m\x1b]8;;\x1b\\. \n\n\x1b[35m▌ \x1b[0m\x1b[35mMarkdown uses email-style > characters for blockquoting.\x1b[0m\x1b[35m \x1b[0m\n\x1b[35m▌ \x1b[0m\x1b[35mLorem ipsum\x1b[0m\x1b[35m \x1b[0m\n\n🌆 \x1b]8;id=0;foo\x1b\\progress\x1b]8;;\x1b\\ \n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34ma=1 \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[38;2;249;38;114;48;2;39;40;34mimport\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mthis\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mfoobar \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n" + assert rendered_markdown == expected + + +def test_inline_code(): + markdown = Markdown( + "inline `import this` code", + inline_code_lexer="python", + inline_code_theme="emacs", + ) + result = render(markdown) + expected = "inline \x1b[1;38;2;170;34;255;48;2;248;248;248mimport\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;0;255;48;2;248;248;248mthis\x1b[0m code \n" + print(result) + print(repr(result)) + assert result == expected + + +if __name__ == "__main__": + markdown = Markdown(MARKDOWN) + rendered = render(markdown) + print(rendered) + print(repr(rendered)) diff --git a/tests/test_markdown_no_hyperlinks.py b/tests/test_markdown_no_hyperlinks.py new file mode 100644 index 0000000..99be518 --- /dev/null +++ b/tests/test_markdown_no_hyperlinks.py @@ -0,0 +1,104 @@ +# coding=utf-8 + +MARKDOWN = """Heading +======= + +Sub-heading +----------- + +### Heading + +#### H4 Heading + +##### H5 Heading + +###### H6 Heading + + +Paragraphs are separated +by a blank line. + +Two spaces at the end of a line +produces a line break. + +Text attributes _italic_, +**bold**, `monospace`. + +Horizontal rule: + +--- + +Bullet list: + + * apples + * oranges + * pears + +Numbered list: + + 1. lather + 2. rinse + 3. repeat + +An [example](http://example.com). + +> Markdown uses email-style > characters for blockquoting. +> +> Lorem ipsum + +![progress](https://github.com/willmcgugan/rich/raw/master/imgs/progress.gif) + + +``` +a=1 +``` + +```python +import this +``` + +```somelang +foobar +``` + +""" + +import io +import re + +from rich.console import Console, RenderableType +from rich.markdown import Markdown + + +re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") + + +def replace_link_ids(render: str) -> str: + """Link IDs have a random ID and system path which is a problem for + reproducible tests. + + """ + return re_link_ids.sub("id=0;foo\x1b", render) + + +def render(renderable: RenderableType) -> str: + console = Console( + width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False + ) + console.print(renderable) + output = replace_link_ids(console.file.getvalue()) + return output + + +def test_markdown_render(): + markdown = Markdown(MARKDOWN, hyperlinks=False) + rendered_markdown = render(markdown) + expected = "╔══════════════════════════════════════════════════════════════════════════════════════════════════╗\n║ \x1b[1mHeading\x1b[0m ║\n╚══════════════════════════════════════════════════════════════════════════════════════════════════╝\n\n\n \x1b[1;4mSub-heading\x1b[0m \n\n \x1b[1mHeading\x1b[0m \n\n \x1b[1;2mH4 Heading\x1b[0m \n\n \x1b[4mH5 Heading\x1b[0m \n\n \x1b[3mH6 Heading\x1b[0m \n\nParagraphs are separated by a blank line. \n\nTwo spaces at the end of a line \nproduces a line break. \n\nText attributes \x1b[3mitalic\x1b[0m, \x1b[1mbold\x1b[0m, \x1b[97;40mmonospace\x1b[0m. \n\nHorizontal rule: \n\n\x1b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\nBullet list: \n\n\x1b[1;33m • \x1b[0mapples \n\x1b[1;33m • \x1b[0moranges \n\x1b[1;33m • \x1b[0mpears \n\nNumbered list: \n\n\x1b[1;33m 1 \x1b[0mlather \n\x1b[1;33m 2 \x1b[0mrinse \n\x1b[1;33m 3 \x1b[0mrepeat \n\nAn \x1b[94mexample\x1b[0m (\x1b[4;34mhttp://example.com\x1b[0m). \n\n\x1b[35m▌ \x1b[0m\x1b[35mMarkdown uses email-style > characters for blockquoting.\x1b[0m\x1b[35m \x1b[0m\n\x1b[35m▌ \x1b[0m\x1b[35mLorem ipsum\x1b[0m\x1b[35m \x1b[0m\n\n🌆 progress \n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34ma=1 \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[38;2;249;38;114;48;2;39;40;34mimport\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mthis\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mfoobar \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n" + assert rendered_markdown == expected + + +if __name__ == "__main__": + markdown = Markdown(MARKDOWN, hyperlinks=False) + rendered = render(markdown) + print(rendered) + print(repr(rendered)) diff --git a/tests/test_markup.py b/tests/test_markup.py new file mode 100644 index 0000000..07c1642 --- /dev/null +++ b/tests/test_markup.py @@ -0,0 +1,151 @@ +import pytest + +from rich.console import Console +from rich.markup import escape, MarkupError, _parse, render, Tag, RE_TAGS +from rich.text import Span + + +def test_re_no_match(): + assert RE_TAGS.match("[True]") == None + assert RE_TAGS.match("[False]") == None + assert RE_TAGS.match("[None]") == None + assert RE_TAGS.match("[1]") == None + assert RE_TAGS.match("[2]") == None + assert RE_TAGS.match("[]") == None + + +def test_re_match(): + assert RE_TAGS.match("[true]") + assert RE_TAGS.match("[false]") + assert RE_TAGS.match("[none]") + assert RE_TAGS.match("[color(1)]") + assert RE_TAGS.match("[#ff00ff]") + assert RE_TAGS.match("[/]") + + +def test_escape(): + # Potential tags + assert escape("foo[bar]") == r"foo\[bar]" + assert escape(r"foo\[bar]") == r"foo\\\[bar]" + + # Not tags (escape not required) + assert escape("[5]") == "[5]" + assert escape("\\[5]") == "\\[5]" + + +def test_render_escape(): + console = Console(width=80, color_system=None) + console.begin_capture() + console.print( + escape(r"[red]"), escape(r"\[red]"), escape(r"\\[red]"), escape(r"\\\[red]") + ) + result = console.end_capture() + expected = r"[red] \[red] \\[red] \\\[red]" + "\n" + assert result == expected + + +def test_parse(): + result = list(_parse(r"[foo]hello[/foo][bar]world[/]\[escaped]")) + expected = [ + (0, None, Tag(name="foo", parameters=None)), + (10, "hello", None), + (10, None, Tag(name="/foo", parameters=None)), + (16, None, Tag(name="bar", parameters=None)), + (26, "world", None), + (26, None, Tag(name="/", parameters=None)), + (29, "[escaped]", None), + ] + print(repr(result)) + assert result == expected + + +def test_parse_link(): + result = list(_parse("[link=foo]bar[/link]")) + expected = [ + (0, None, Tag(name="link", parameters="foo")), + (13, "bar", None), + (13, None, Tag(name="/link", parameters=None)), + ] + assert result == expected + + +def test_render(): + result = render("[bold]FOO[/bold]") + assert str(result) == "FOO" + assert result.spans == [Span(0, 3, "bold")] + + +def test_render_not_tags(): + result = render('[[1], [1,2,3,4], ["hello"], [None], [False], [True]] []') + assert str(result) == '[[1], [1,2,3,4], ["hello"], [None], [False], [True]] []' + assert result.spans == [] + + +def test_render_link(): + result = render("[link=foo]FOO[/link]") + assert str(result) == "FOO" + assert result.spans == [Span(0, 3, "link foo")] + + +def test_render_combine(): + result = render("[green]X[blue]Y[/blue]Z[/green]") + assert str(result) == "XYZ" + assert result.spans == [ + Span(0, 3, "green"), + Span(1, 2, "blue"), + ] + + +def test_render_overlap(): + result = render("[green]X[bold]Y[/green]Z[/bold]") + assert str(result) == "XYZ" + assert result.spans == [ + Span(0, 2, "green"), + Span(1, 3, "bold"), + ] + + +def test_render_close(): + result = render("[bold]X[/]Y") + assert str(result) == "XY" + assert result.spans == [Span(0, 1, "bold")] + + +def test_render_close_ambiguous(): + result = render("[green]X[bold]Y[/]Z[/]") + assert str(result) == "XYZ" + assert result.spans == [Span(0, 3, "green"), Span(1, 2, "bold")] + + +def test_markup_error(): + with pytest.raises(MarkupError): + assert render("foo[/]") + with pytest.raises(MarkupError): + assert render("foo[/bar]") + with pytest.raises(MarkupError): + assert render("[foo]hello[/bar]") + + +def test_escape_escape(): + # Escaped escapes (i.e. double backslash)should be treated as literal + result = render(r"\\[bold]FOO") + assert str(result) == r"\FOO" + + # Single backslash makes the tag literal + result = render(r"\[bold]FOO") + assert str(result) == "[bold]FOO" + + # Double backslash produces a backslash + result = render(r"\\[bold]some text[/]") + assert str(result) == r"\some text" + + # Triple backslash parsed as literal backslash plus escaped tag + result = render(r"\\\[bold]some text\[/]") + assert str(result) == r"\[bold]some text[/]" + + # Backslash escaping only happens when preceding a tag + result = render(r"\\") + assert str(result) == r"\\" + + result = render(r"\\\\") + assert str(result) == r"\\\\" diff --git a/tests/test_measure.py b/tests/test_measure.py new file mode 100644 index 0000000..664701f --- /dev/null +++ b/tests/test_measure.py @@ -0,0 +1,42 @@ +from rich.text import Text +import pytest + +from rich.errors import NotRenderableError +from rich.console import Console +from rich.measure import Measurement, measure_renderables + + +def test_span(): + measurement = Measurement(10, 100) + assert measurement.span == 90 + + +def test_no_renderable(): + console = Console() + text = Text() + + with pytest.raises(NotRenderableError): + Measurement.get(console, None, console.width) + + +def test_null_get(): + # Test negative console.width passed into get method + assert Measurement.get(Console(width=-1), None) == Measurement(0, 0) + # Test negative max_width passed into get method + assert Measurement.get(Console(), None, -1) == Measurement(0, 0) + + +def test_measure_renderables(): + # Test measure_renderables returning a null Measurement object + assert measure_renderables(Console(), None, None) == Measurement(0, 0) + # Test measure_renderables returning a valid Measurement object + assert measure_renderables(Console(width=1), ["test"], 1) == Measurement(1, 1) + + +def test_clamp(): + measurement = Measurement(20, 100) + assert measurement.clamp(10, 50) == Measurement(20, 50) + assert measurement.clamp(30, 50) == Measurement(30, 50) + assert measurement.clamp(None, 50) == Measurement(20, 50) + assert measurement.clamp(30, None) == Measurement(30, 100) + assert measurement.clamp(None, None) == Measurement(20, 100) diff --git a/tests/test_padding.py b/tests/test_padding.py new file mode 100644 index 0000000..d4508e9 --- /dev/null +++ b/tests/test_padding.py @@ -0,0 +1,59 @@ +import pytest + +from rich.padding import Padding +from rich.console import Console, ConsoleDimensions, ConsoleOptions +from rich.style import Style +from rich.segment import Segment + + +def test_repr(): + padding = Padding("foo", (1, 2)) + assert isinstance(repr(padding), str) + + +def test_indent(): + indent_result = Padding.indent("test", 4) + assert indent_result.top == 0 + assert indent_result.right == 0 + assert indent_result.bottom == 0 + assert indent_result.left == 4 + + +def test_unpack(): + assert Padding.unpack(3) == (3, 3, 3, 3) + assert Padding.unpack((3,)) == (3, 3, 3, 3) + assert Padding.unpack((3, 4)) == (3, 4, 3, 4) + assert Padding.unpack((3, 4, 5, 6)) == (3, 4, 5, 6) + with pytest.raises(ValueError): + Padding.unpack((1, 2, 3)) + + +def test_expand_false(): + console = Console(width=100, color_system=None) + console.begin_capture() + console.print(Padding("foo", 1, expand=False)) + assert console.end_capture() == " \n foo \n \n" + + +def test_rich_console(): + renderable = "test renderable" + style = Style(color="red") + options = ConsoleOptions( + ConsoleDimensions(80, 25), + legacy_windows=False, + min_width=10, + max_width=20, + is_terminal=False, + encoding="utf-8", + ) + + expected_outputs = [ + Segment(renderable, style=style), + Segment(" " * (20 - len(renderable)), style=style), + Segment("\n", style=None), + ] + padding_generator = Padding(renderable, style=style).__rich_console__( + Console(), options + ) + for output, expected in zip(padding_generator, expected_outputs): + assert output == expected diff --git a/tests/test_palette.py b/tests/test_palette.py new file mode 100644 index 0000000..8ddd7cc --- /dev/null +++ b/tests/test_palette.py @@ -0,0 +1,8 @@ +from rich._palettes import STANDARD_PALETTE +from rich.table import Table + + +def test_rich_cast(): + table = STANDARD_PALETTE.__rich__() + assert isinstance(table, Table) + assert table.row_count == 16 diff --git a/tests/test_panel.py b/tests/test_panel.py new file mode 100644 index 0000000..da6ad78 --- /dev/null +++ b/tests/test_panel.py @@ -0,0 +1,62 @@ +import io +from rich.console import Console +from rich.measure import Measurement +from rich.panel import Panel + +import pytest + +tests = [ + Panel("Hello, World", padding=0), + Panel("Hello, World", expand=False, padding=0), + Panel.fit("Hello, World", padding=0), + Panel("Hello, World", width=8, padding=0), + Panel(Panel("Hello, World", padding=0), padding=0), + Panel("Hello, World", title="FOO", padding=0), +] + +expected = [ + "╭────────────────────────────────────────────────╮\n│Hello, World │\n╰────────────────────────────────────────────────╯\n", + "╭────────────╮\n│Hello, World│\n╰────────────╯\n", + "╭────────────╮\n│Hello, World│\n╰────────────╯\n", + "╭──────╮\n│Hello,│\n│World │\n╰──────╯\n", + "╭────────────────────────────────────────────────╮\n│╭──────────────────────────────────────────────╮│\n││Hello, World ││\n│╰──────────────────────────────────────────────╯│\n╰────────────────────────────────────────────────╯\n", + "╭───────────────────── FOO ──────────────────────╮\n│Hello, World │\n╰────────────────────────────────────────────────╯\n", +] + + +def render(panel, width=50) -> str: + console = Console(file=io.StringIO(), width=50, legacy_windows=False) + console.print(panel) + return console.file.getvalue() + + +@pytest.mark.parametrize("panel,expected", zip(tests, expected)) +def test_render_panel(panel, expected): + assert render(panel) == expected + + +def test_console_width(): + console = Console(file=io.StringIO(), width=50, legacy_windows=False) + panel = Panel("Hello, World", expand=False) + min_width, max_width = panel.__rich_measure__(console, 50) + assert min_width == 16 + assert max_width == 16 + + +def test_fixed_width(): + console = Console(file=io.StringIO(), width=50, legacy_windows=False) + panel = Panel("Hello World", width=20) + min_width, max_width = panel.__rich_measure__(console, 100) + assert min_width == 20 + assert max_width == 20 + + +if __name__ == "__main__": + expected = [] + for panel in tests: + result = render(panel) + print(result) + expected.append(result) + print("--") + print() + print(f"expected={repr(expected)}") diff --git a/tests/test_pick.py b/tests/test_pick.py new file mode 100644 index 0000000..9261d6f --- /dev/null +++ b/tests/test_pick.py @@ -0,0 +1,11 @@ +from rich._pick import pick_bool + + +def test_pick_bool(): + assert pick_bool(False, True) == False + assert pick_bool(None, True) == True + assert pick_bool(True, None) == True + assert pick_bool(False, None) == False + assert pick_bool(None, None) == False + assert pick_bool(None, None, False, True) == False + assert pick_bool(None, None, True, False) == True diff --git a/tests/test_pretty.py b/tests/test_pretty.py new file mode 100644 index 0000000..4ba8c27 --- /dev/null +++ b/tests/test_pretty.py @@ -0,0 +1,145 @@ +from array import array +from collections import defaultdict +import io +import sys + +from rich.console import Console +from rich.pretty import install, Pretty, pprint, pretty_repr, Node + + +def test_install(): + console = Console(file=io.StringIO()) + dh = sys.displayhook + install(console) + sys.displayhook("foo") + assert console.file.getvalue() == "'foo'\n" + assert sys.displayhook is not dh + + +def test_pretty(): + test = { + "foo": [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}], + "bar": {"egg": "baz", "words": ["Hello World"] * 10}, + False: "foo", + True: "", + "text": ("Hello World", "foo bar baz egg"), + } + + result = pretty_repr(test, max_width=80) + print(result) + print(repr(result)) + expected = "{\n 'foo': [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}],\n 'bar': {\n 'egg': 'baz',\n 'words': [\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World'\n ]\n },\n False: 'foo',\n True: '',\n 'text': ('Hello World', 'foo bar baz egg')\n}" + print(expected) + assert result == expected + + +def test_small_width(): + test = ["Hello world! 12345"] + result = pretty_repr(test, max_width=10) + expected = "[\n 'Hello world! 12345'\n]" + assert result == expected + + +def test_broken_repr(): + class BrokenRepr: + def __repr__(self): + 1 / 0 + + test = [BrokenRepr()] + result = pretty_repr(test) + expected = "[<repr-error 'division by zero'>]" + assert result == expected + + +def test_recursive(): + test = [] + test.append(test) + result = pretty_repr(test) + expected = "[...]" + assert result == expected + + +def test_defaultdict(): + test_dict = defaultdict(int, {"foo": 2}) + result = pretty_repr(test_dict) + assert result == "defaultdict(<class 'int'>, {'foo': 2})" + + +def test_array(): + test_array = array("I", [1, 2, 3]) + result = pretty_repr(test_array) + assert result == "array('I', [1, 2, 3])" + + +def test_tuple_of_one(): + assert pretty_repr((1,)) == "(1,)" + + +def test_node(): + node = Node("abc") + assert pretty_repr(node) == "abc: " + + +def test_indent_lines(): + console = Console(width=100, color_system=None) + console.begin_capture() + console.print(Pretty([100, 200], indent_guides=True), width=8) + expected = """\ +[ +│ 100, +│ 200 +] +""" + result = console.end_capture() + print(repr(result)) + print(result) + assert result == expected + + +def test_pprint(): + console = Console(color_system=None) + console.begin_capture() + pprint(1, console=console) + assert console.end_capture() == "1\n" + + +def test_pprint_max_values(): + console = Console(color_system=None) + console.begin_capture() + pprint([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], console=console, max_length=2) + assert console.end_capture() == "[1, 2, ... +8]\n" + + +def test_pprint_max_items(): + console = Console(color_system=None) + console.begin_capture() + pprint({"foo": 1, "bar": 2, "egg": 3}, console=console, max_length=2) + assert console.end_capture() == """{'foo': 1, 'bar': 2, ... +1}\n""" + + +def test_pprint_max_string(): + console = Console(color_system=None) + console.begin_capture() + pprint(["Hello" * 20], console=console, max_string=8) + assert console.end_capture() == """['HelloHel'+92]\n""" + + +def test_tuples(): + console = Console(color_system=None) + console.begin_capture() + pprint((1,), console=console) + pprint((1,), expand_all=True, console=console) + pprint(((1,),), expand_all=True, console=console) + result = console.end_capture() + print(repr(result)) + expected = "(1,)\n(\n│ 1,\n)\n(\n│ (\n│ │ 1,\n│ ),\n)\n" + assert result == expected + + +def test_newline(): + console = Console(color_system=None) + console.begin_capture() + console.print(Pretty((1,), insert_line=True, expand_all=True)) + result = console.end_capture() + expected = "\n(\n 1,\n)\n" + assert result == expected diff --git a/tests/test_progress.py b/tests/test_progress.py new file mode 100644 index 0000000..565e971 --- /dev/null +++ b/tests/test_progress.py @@ -0,0 +1,430 @@ +# encoding=utf-8 + +import io +from time import sleep + +import pytest + +from rich.progress_bar import ProgressBar +from rich.console import Console +from rich.highlighter import NullHighlighter +from rich.progress import ( + BarColumn, + FileSizeColumn, + TotalFileSizeColumn, + DownloadColumn, + TransferSpeedColumn, + RenderableColumn, + SpinnerColumn, + Progress, + Task, + TextColumn, + TimeElapsedColumn, + TimeRemainingColumn, + track, + _TrackThread, + TaskID, +) +from rich.text import Text + + +class MockClock: + """A clock that is manually advanced.""" + + def __init__(self, time=0.0, auto=True) -> None: + self.time = time + self.auto = auto + + def __call__(self) -> float: + try: + return self.time + finally: + if self.auto: + self.time += 1 + + def tick(self, advance: float = 1) -> None: + self.time += advance + + +def test_bar_columns(): + bar_column = BarColumn(100) + assert bar_column.bar_width == 100 + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + bar = bar_column(task) + assert isinstance(bar, ProgressBar) + assert bar.completed == 20 + assert bar.total == 100 + + +def test_text_column(): + text_column = TextColumn("[b]foo", highlighter=NullHighlighter()) + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + text = text_column.render(task) + assert str(text) == "foo" + + text_column = TextColumn("[b]bar", markup=False) + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + text = text_column.render(task) + assert text == Text("[b]bar") + + +def test_time_remaining_column(): + class FakeTask(Task): + time_remaining = 60 + + column = TimeRemainingColumn() + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + text = column(task) + assert str(text) == "-:--:--" + + text = column(FakeTask(1, "test", 100, 20, _get_time=lambda: 1.0)) + assert str(text) == "0:01:00" + + +def test_renderable_column(): + column = RenderableColumn("foo") + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + assert column.render(task) == "foo" + + +def test_spinner_column(): + column = SpinnerColumn() + column.set_spinner("dots2") + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + result = column.render(task) + print(repr(result)) + expected = "⡿" + assert str(result) == expected + + +def test_download_progress_uses_decimal_units() -> None: + + column = DownloadColumn() + test_task = Task(1, "test", 1000, 500, _get_time=lambda: 1.0) + rendered_progress = str(column.render(test_task)) + expected = "0.5/1.0 KB" + assert rendered_progress == expected + + +def test_download_progress_uses_binary_units() -> None: + + column = DownloadColumn(binary_units=True) + test_task = Task(1, "test", 1024, 512, _get_time=lambda: 1.0) + rendered_progress = str(column.render(test_task)) + expected = "0.5/1.0 KiB" + assert rendered_progress == expected + + +def test_task_ids(): + progress = make_progress() + assert progress.task_ids == [0, 1, 2, 4] + + +def test_finished(): + progress = make_progress() + assert not progress.finished + + +def make_progress() -> Progress: + _time = 0.0 + + def fake_time(): + nonlocal _time + try: + return _time + finally: + _time += 1 + + console = Console( + file=io.StringIO(), + force_terminal=True, + color_system="truecolor", + width=80, + legacy_windows=False, + ) + progress = Progress(console=console, get_time=fake_time, auto_refresh=False) + task1 = progress.add_task("foo") + task2 = progress.add_task("bar", total=30) + progress.advance(task2, 16) + task3 = progress.add_task("baz", visible=False) + task4 = progress.add_task("egg") + progress.remove_task(task4) + task4 = progress.add_task("foo2", completed=50, start=False) + progress.stop_task(task4) + progress.start_task(task4) + progress.update( + task4, total=200, advance=50, completed=200, visible=True, refresh=True + ) + progress.stop_task(task4) + return progress + + +def render_progress() -> str: + progress = make_progress() + progress.start() # superfluous noop + with progress: + pass + progress.stop() # superfluous noop + progress_render = progress.console.file.getvalue() + return progress_render + + +def test_expand_bar() -> None: + console = Console( + file=io.StringIO(), + force_terminal=True, + width=10, + color_system="truecolor", + legacy_windows=False, + ) + progress = Progress( + BarColumn(bar_width=None), + console=console, + get_time=lambda: 1.0, + auto_refresh=False, + ) + progress.add_task("foo") + with progress: + pass + expected = "\x1b[?25l\x1b[38;5;237m━━━━━━━━━━\x1b[0m\r\x1b[2K\x1b[38;5;237m━━━━━━━━━━\x1b[0m\n\x1b[?25h" + render_result = console.file.getvalue() + print("RESULT\n", repr(render_result)) + print("EXPECTED\n", repr(expected)) + assert render_result == expected + + +def test_render() -> None: + expected = "\x1b[?25lfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\nfoo2 \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\nfoo2 \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\n\x1b[?25h" + render_result = render_progress() + print(repr(render_result)) + assert render_result == expected + + +def test_track() -> None: + + console = Console( + file=io.StringIO(), + force_terminal=True, + width=60, + color_system="truecolor", + legacy_windows=False, + ) + test = ["foo", "bar", "baz"] + expected_values = iter(test) + for value in track( + test, "test", console=console, auto_refresh=False, get_time=MockClock(auto=True) + ): + assert value == next(expected_values) + result = console.file.getvalue() + print(repr(result)) + expected = "\x1b[?25l\r\x1b[2Ktest \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 33%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━\x1b[0m \x1b[35m 67%\x1b[0m \x1b[36m0:00:06\x1b[0m\r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\n\x1b[?25h" + print("--") + print("RESULT:") + print(result) + print(repr(result)) + print("EXPECTED:") + print(expected) + print(repr(expected)) + + assert result == expected + + with pytest.raises(ValueError): + for n in track(5): + pass + + +def test_progress_track() -> None: + console = Console( + file=io.StringIO(), + force_terminal=True, + width=60, + color_system="truecolor", + legacy_windows=False, + ) + progress = Progress( + console=console, auto_refresh=False, get_time=MockClock(auto=True) + ) + test = ["foo", "bar", "baz"] + expected_values = iter(test) + with progress: + for value in progress.track(test, description="test"): + assert value == next(expected_values) + result = console.file.getvalue() + print(repr(result)) + expected = "\x1b[?25l\r\x1b[2Ktest \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 33%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━\x1b[0m \x1b[35m 67%\x1b[0m \x1b[36m0:00:06\x1b[0m\r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m\n\x1b[?25h" + + print(expected) + print(repr(expected)) + print(result) + print(repr(result)) + + assert result == expected + + with pytest.raises(ValueError): + for n in progress.track(5): + pass + + +def test_columns() -> None: + + console = Console( + file=io.StringIO(), + force_terminal=True, + width=80, + log_time_format="[TIME]", + color_system="truecolor", + legacy_windows=False, + log_path=False, + ) + progress = Progress( + "test", + TextColumn("{task.description}"), + BarColumn(bar_width=None), + TimeRemainingColumn(), + TimeElapsedColumn(), + FileSizeColumn(), + TotalFileSizeColumn(), + DownloadColumn(), + TransferSpeedColumn(), + transient=True, + console=console, + auto_refresh=False, + get_time=MockClock(), + ) + task1 = progress.add_task("foo", total=10) + task2 = progress.add_task("bar", total=7) + with progress: + for n in range(4): + progress.advance(task1, 3) + progress.advance(task2, 4) + print("foo") + console.log("hello") + console.print("world") + progress.refresh() + from .render import replace_link_ids + + result = replace_link_ids(console.file.getvalue()) + print(repr(result)) + expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:01:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s\x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:45\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m1 byte/s\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:01:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s\x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:45\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m1 byte/s\x1b[0m\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K" + assert result == expected + + +def test_task_create() -> None: + task = Task(TaskID(1), "foo", 100, 0, _get_time=lambda: 1) + assert task.elapsed is None + assert not task.finished + assert task.percentage == 0.0 + assert task.speed is None + assert task.time_remaining is None + + +def test_task_start() -> None: + current_time = 1 + + def get_time(): + nonlocal current_time + return current_time + + task = Task(TaskID(1), "foo", 100, 0, _get_time=get_time) + task.start_time = get_time() + assert task.started == True + assert task.elapsed == 0 + current_time += 1 + assert task.elapsed == 1 + current_time += 1 + task.stop_time = get_time() + current_time += 1 + assert task.elapsed == 2 + + +def test_task_zero_total() -> None: + task = Task(TaskID(1), "foo", 0, 0, _get_time=lambda: 1) + assert task.percentage == 0 + + +def test_progress_create() -> None: + progress = Progress() + assert progress.finished + assert progress.tasks == [] + assert progress.task_ids == [] + + +def test_track_thread() -> None: + progress = Progress() + task_id = progress.add_task("foo") + track_thread = _TrackThread(progress, task_id, 0.1) + assert track_thread.completed == 0 + from time import sleep + + with track_thread: + track_thread.completed = 1 + sleep(0.3) + assert progress.tasks[task_id].completed >= 1 + track_thread.completed += 1 + + +def test_reset() -> None: + progress = Progress() + task_id = progress.add_task("foo") + progress.advance(task_id, 1) + progress.advance(task_id, 1) + progress.advance(task_id, 1) + progress.advance(task_id, 7) + task = progress.tasks[task_id] + assert task.completed == 10 + progress.reset( + task_id, + total=200, + completed=20, + visible=False, + description="bar", + example="egg", + ) + assert task.total == 200 + assert task.completed == 20 + assert task.visible == False + assert task.description == "bar" + assert task.fields == {"example": "egg"} + assert not task._progress + + +def test_progress_max_refresh() -> None: + """Test max_refresh argment.""" + time = 0.0 + + def get_time() -> float: + nonlocal time + try: + return time + finally: + time = time + 1.0 + + console = Console( + color_system=None, width=80, legacy_windows=False, force_terminal=True + ) + column = TextColumn("{task.description}") + column.max_refresh = 3 + progress = Progress( + column, + get_time=get_time, + auto_refresh=False, + console=console, + ) + console.begin_capture() + with progress: + task_id = progress.add_task("start") + for tick in range(6): + progress.update(task_id, description=f"tick {tick}") + progress.refresh() + result = console.end_capture() + print(repr(result)) + assert ( + result + == "\x1b[?25l\r\x1b[2Kstart\r\x1b[2Kstart\r\x1b[2Ktick 1\r\x1b[2Ktick 1\r\x1b[2Ktick 3\r\x1b[2Ktick 3\r\x1b[2Ktick 5\r\x1b[2Ktick 5\n\x1b[?25h" + ) + + +if __name__ == "__main__": + _render = render_progress() + print(_render) + print(repr(_render)) diff --git a/tests/test_prompt.py b/tests/test_prompt.py new file mode 100644 index 0000000..9a41cc3 --- /dev/null +++ b/tests/test_prompt.py @@ -0,0 +1,95 @@ +import io + +from rich.console import Console +from rich.prompt import Prompt, IntPrompt, Confirm + + +def test_prompt_str(): + INPUT = "egg\nfoo" + console = Console(file=io.StringIO()) + name = Prompt.ask( + "what is your name", + console=console, + choices=["foo", "bar"], + default="baz", + stream=io.StringIO(INPUT), + ) + assert name == "foo" + expected = "what is your name [foo/bar] (baz): Please select one of the available options\nwhat is your name [foo/bar] (baz): " + output = console.file.getvalue() + print(repr(output)) + assert output == expected + + +def test_prompt_str_default(): + INPUT = "" + console = Console(file=io.StringIO()) + name = Prompt.ask( + "what is your name", + console=console, + default="Will", + stream=io.StringIO(INPUT), + ) + assert name == "Will" + expected = "what is your name (Will): " + output = console.file.getvalue() + print(repr(output)) + assert output == expected + + +def test_prompt_int(): + INPUT = "foo\n100" + console = Console(file=io.StringIO()) + number = IntPrompt.ask( + "Enter a number", + console=console, + stream=io.StringIO(INPUT), + ) + assert number == 100 + expected = "Enter a number: Please enter a valid integer number\nEnter a number: " + output = console.file.getvalue() + print(repr(output)) + assert output == expected + + +def test_prompt_confirm_no(): + INPUT = "foo\nNO\nn" + console = Console(file=io.StringIO()) + answer = Confirm.ask( + "continue", + console=console, + stream=io.StringIO(INPUT), + ) + assert answer is False + expected = "continue [y/n]: Please enter Y or N\ncontinue [y/n]: Please enter Y or N\ncontinue [y/n]: " + output = console.file.getvalue() + print(repr(output)) + assert output == expected + + +def test_prompt_confirm_yes(): + INPUT = "foo\nNO\ny" + console = Console(file=io.StringIO()) + answer = Confirm.ask( + "continue", + console=console, + stream=io.StringIO(INPUT), + ) + assert answer is True + expected = "continue [y/n]: Please enter Y or N\ncontinue [y/n]: Please enter Y or N\ncontinue [y/n]: " + output = console.file.getvalue() + print(repr(output)) + assert output == expected + + +def test_prompt_confirm_default(): + INPUT = "foo\nNO\ny" + console = Console(file=io.StringIO()) + answer = Confirm.ask( + "continue", console=console, stream=io.StringIO(INPUT), default=True + ) + assert answer is True + expected = "continue [y/n] (y): Please enter Y or N\ncontinue [y/n] (y): Please enter Y or N\ncontinue [y/n] (y): " + output = console.file.getvalue() + print(repr(output)) + assert output == expected diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 0000000..310b994 --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,35 @@ +import io + +from rich.abc import RichRenderable +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + + +class Foo: + def __rich__(self) -> Text: + return Text("Foo") + + +def test_rich_cast(): + foo = Foo() + console = Console(file=io.StringIO()) + console.print(foo) + assert console.file.getvalue() == "Foo\n" + + +def test_rich_cast_container(): + foo = Foo() + console = Console(file=io.StringIO(), legacy_windows=False) + console.print(Panel.fit(foo, padding=0)) + assert console.file.getvalue() == "╭───╮\n│Foo│\n╰───╯\n" + + +def test_abc(): + foo = Foo() + assert isinstance(foo, RichRenderable) + assert isinstance(Text("hello"), RichRenderable) + assert isinstance(Panel("hello"), RichRenderable) + assert not isinstance(foo, str) + assert not isinstance("foo", RichRenderable) + assert not isinstance([], RichRenderable) diff --git a/tests/test_ratio.py b/tests/test_ratio.py new file mode 100644 index 0000000..8a44e8c --- /dev/null +++ b/tests/test_ratio.py @@ -0,0 +1,50 @@ +import pytest +from typing import NamedTuple, Optional + +from rich._ratio import ratio_reduce, ratio_resolve + + +class Edge(NamedTuple): + size: Optional[int] = None + ratio: int = 1 + minimum_size: int = 1 + + +@pytest.mark.parametrize( + "total,ratios,maximums,values,result", + [ + (20, [2, 4], [20, 20], [5, 5], [-2, -8]), + (20, [2, 4], [1, 1], [5, 5], [4, 4]), + (20, [2, 4], [1, 1], [2, 2], [1, 1]), + (3, [2, 4], [3, 3], [2, 2], [1, 0]), + (3, [2, 4], [3, 3], [0, 0], [-1, -2]), + (3, [0, 0], [3, 3], [4, 4], [4, 4]), + ], +) +def test_ratio_reduce(total, ratios, maximums, values, result): + assert ratio_reduce(total, ratios, maximums, values) == result + + +def test_ratio_resolve(): + assert ratio_resolve(100, []) == [] + assert ratio_resolve(100, [Edge(size=100), Edge(ratio=1)]) == [100, 1] + assert ratio_resolve(100, [Edge(ratio=1)]) == [100] + assert ratio_resolve(100, [Edge(ratio=1), Edge(ratio=1)]) == [50, 50] + assert ratio_resolve(100, [Edge(size=20), Edge(ratio=1), Edge(ratio=1)]) == [ + 20, + 40, + 40, + ] + assert ratio_resolve(100, [Edge(size=40), Edge(ratio=2), Edge(ratio=1)]) == [ + 40, + 40, + 20, + ] + assert ratio_resolve( + 100, [Edge(size=40), Edge(ratio=2), Edge(ratio=1, minimum_size=25)] + ) == [40, 35, 25] + assert ratio_resolve(100, [Edge(ratio=1), Edge(ratio=1), Edge(ratio=1)]) == [ + 33, + 33, + 34, + ] diff --git a/tests/test_rich_print.py b/tests/test_rich_print.py new file mode 100644 index 0000000..18467c6 --- /dev/null +++ b/tests/test_rich_print.py @@ -0,0 +1,42 @@ +import io + +import rich +from rich.console import Console + + +def test_get_console(): + console = rich.get_console() + assert isinstance(console, Console) + + +def test_reconfigure_console(): + rich.reconfigure(width=100) + assert rich.get_console().width == 100 + + +def test_rich_print(): + console = rich.get_console() + output = io.StringIO() + backup_file = console.file + try: + console.file = output + rich.print("foo", "bar") + rich.print("foo\n") + rich.print("foo\n\n") + assert output.getvalue() == "foo bar\nfoo\n\nfoo\n\n\n" + finally: + console.file = backup_file + + +def test_rich_print_X(): + console = rich.get_console() + output = io.StringIO() + backup_file = console.file + try: + console.file = output + rich.print("foo") + rich.print("fooX") + rich.print("fooXX") + assert output.getvalue() == "foo\nfooX\nfooXX\n" + finally: + console.file = backup_file diff --git a/tests/test_rule.py b/tests/test_rule.py new file mode 100644 index 0000000..d5edcf4 --- /dev/null +++ b/tests/test_rule.py @@ -0,0 +1,83 @@ +import io + +import pytest + +from rich.console import Console +from rich.rule import Rule +from rich.text import Text + + +def test_rule(): + console = Console( + width=16, file=io.StringIO(), force_terminal=True, legacy_windows=False + ) + console.print(Rule()) + console.print(Rule("foo")) + console.rule(Text("foo", style="bold")) + console.rule("foobarbazeggfoobarbazegg") + expected = "\x1b[92m────────────────\x1b[0m\n" + expected += "\x1b[92m───── \x1b[0mfoo\x1b[92m ──────\x1b[0m\n" + expected += "\x1b[92m───── \x1b[0m\x1b[1mfoo\x1b[0m\x1b[92m ──────\x1b[0m\n" + expected += "\x1b[92m─ \x1b[0mfoobarbazeg…\x1b[92m ─\x1b[0m\n" + + result = console.file.getvalue() + assert result == expected + + +def test_rule_error(): + console = Console(width=16, file=io.StringIO(), legacy_windows=False) + with pytest.raises(ValueError): + console.rule("foo", align="foo") + + +def test_rule_align(): + console = Console(width=16, file=io.StringIO(), legacy_windows=False) + console.rule("foo") + console.rule("foo", align="left") + console.rule("foo", align="center") + console.rule("foo", align="right") + console.rule() + result = console.file.getvalue() + print(repr(result)) + expected = "───── foo ──────\nfoo ────────────\n───── foo ──────\n──────────── foo\n────────────────\n" + assert result == expected + + +def test_rule_cjk(): + console = Console( + width=16, + file=io.StringIO(), + force_terminal=True, + color_system=None, + legacy_windows=False, + ) + console.rule("欢迎!") + expected = "──── 欢迎! ────\n" + assert console.file.getvalue() == expected + + +def test_characters(): + console = Console( + width=16, + file=io.StringIO(), + force_terminal=True, + color_system=None, + legacy_windows=False, + ) + console.rule(characters="+*") + console.rule("foo", characters="+*") + console.print(Rule(characters=".,")) + expected = "+*+*+*+*+*+*+*+*\n" + expected += "+*+*+ foo +*+*+*\n" + expected += ".,.,.,.,.,.,.,.,\n" + assert console.file.getvalue() == expected + + +def test_repr(): + rule = Rule("foo") + assert isinstance(repr(rule), str) + + +def test_error(): + with pytest.raises(ValueError): + Rule(characters="") diff --git a/tests/test_screen.py b/tests/test_screen.py new file mode 100644 index 0000000..7596c3a --- /dev/null +++ b/tests/test_screen.py @@ -0,0 +1,12 @@ +from rich.console import Console +from rich.screen import Screen + + +def test_screen(): + console = Console(color_system=None, width=20, height=5) + with console.capture() as capture: + console.print(Screen("foo\nbar\nbaz\nfoo\nbar\nbaz\foo")) + result = capture.get() + print(repr(result)) + expected = "foo \nbar \nbaz \nfoo \nbar " + assert result == expected diff --git a/tests/test_segment.py b/tests/test_segment.py new file mode 100644 index 0000000..7e7a3b3 --- /dev/null +++ b/tests/test_segment.py @@ -0,0 +1,117 @@ +from rich.segment import Segment +from rich.style import Style + + +def test_repr(): + assert repr(Segment("foo")) == "Segment('foo', None)" + assert repr(Segment.control("foo")) == "Segment.control('foo', None)" + + +def test_line(): + assert Segment.line() == Segment("\n") + + +def test_apply_style(): + segments = [Segment("foo"), Segment("bar", Style(bold=True))] + assert Segment.apply_style(segments, None) is segments + assert list(Segment.apply_style(segments, Style(italic=True))) == [ + Segment("foo", Style(italic=True)), + Segment("bar", Style(italic=True, bold=True)), + ] + + +def test_split_lines(): + lines = [Segment("Hello\nWorld")] + assert list(Segment.split_lines(lines)) == [[Segment("Hello")], [Segment("World")]] + + +def test_split_and_crop_lines(): + assert list( + Segment.split_and_crop_lines([Segment("Hello\nWorld!\n"), Segment("foo")], 4) + ) == [ + [Segment("Hell"), Segment("\n", None)], + [Segment("Worl"), Segment("\n", None)], + [Segment("foo"), Segment(" ")], + ] + + +def test_adjust_line_length(): + line = [Segment("Hello", "foo")] + assert Segment.adjust_line_length(line, 10, style="bar") == [ + Segment("Hello", "foo"), + Segment(" ", "bar"), + ] + + line = [Segment("H"), Segment("ello, World!")] + assert Segment.adjust_line_length(line, 5) == [Segment("H"), Segment("ello")] + + line = [Segment("Hello")] + assert Segment.adjust_line_length(line, 5) == line + + +def test_get_line_length(): + assert Segment.get_line_length([Segment("foo"), Segment("bar")]) == 6 + + +def test_get_shape(): + assert Segment.get_shape([[Segment("Hello")]]) == (5, 1) + assert Segment.get_shape([[Segment("Hello")], [Segment("World!")]]) == (6, 2) + + +def test_set_shape(): + assert Segment.set_shape([[Segment("Hello")]], 10) == [ + [Segment("Hello"), Segment(" ")] + ] + assert Segment.set_shape([[Segment("Hello")]], 10, 2) == [ + [Segment("Hello"), Segment(" ")], + [Segment(" " * 10)], + ] + + +def test_simplify(): + assert list( + Segment.simplify([Segment("Hello"), Segment(" "), Segment("World!")]) + ) == [Segment("Hello World!")] + assert list( + Segment.simplify( + [Segment("Hello", "red"), Segment(" ", "red"), Segment("World!", "blue")] + ) + ) == [Segment("Hello ", "red"), Segment("World!", "blue")] + assert list(Segment.simplify([])) == [] + + +def test_filter_control(): + segments = [Segment("foo"), Segment("bar", is_control=True)] + assert list(Segment.filter_control(segments)) == [Segment("foo")] + assert list(Segment.filter_control(segments, is_control=True)) == [ + Segment("bar", is_control=True) + ] + + +def test_strip_styles(): + segments = [Segment("foo", Style(bold=True))] + assert list(Segment.strip_styles(segments)) == [Segment("foo", None)] + + +def test_strip_links(): + segments = [Segment("foo", Style(bold=True, link="https://www.example.org"))] + assert list(Segment.strip_links(segments)) == [Segment("foo", Style(bold=True))] + + +def test_remove_color(): + segments = [ + Segment("foo", Style(bold=True, color="red")), + Segment("bar", None), + ] + assert list(Segment.remove_color(segments)) == [ + Segment("foo", Style(bold=True)), + Segment("bar", None), + ] + + +def test_make_control(): + segments = [Segment("foo"), Segment("bar")] + assert Segment.make_control(segments) == [ + Segment.control("foo"), + Segment.control("bar"), + ] diff --git a/tests/test_spinner.py b/tests/test_spinner.py new file mode 100644 index 0000000..7f2b0a1 --- /dev/null +++ b/tests/test_spinner.py @@ -0,0 +1,42 @@ +from time import time +import pytest + +from rich.console import Console +from rich.measure import Measurement +from rich.spinner import Spinner + + +def test_spinner_create(): + spinner = Spinner("dots") + assert spinner.time == 0.0 + with pytest.raises(KeyError): + Spinner("foobar") + + +def test_spinner_render(): + time = 0.0 + + def get_time(): + nonlocal time + return time + + console = Console( + width=80, color_system=None, force_terminal=True, get_time=get_time + ) + console.begin_capture() + spinner = Spinner("dots", "Foo") + console.print(spinner) + time += 80 / 1000 + console.print(spinner) + result = console.end_capture() + print(repr(result)) + expected = "⠋ Foo\n⠙ Foo\n" + assert result == expected + + +def test_rich_measure(): + console = Console(width=80, color_system=None, force_terminal=True) + spinner = Spinner("dots", "Foo") + min_width, max_width = Measurement.get(console, spinner, 80) + assert min_width == 3 + assert max_width == 5 diff --git a/tests/test_stack.py b/tests/test_stack.py new file mode 100644 index 0000000..5bbb885 --- /dev/null +++ b/tests/test_stack.py @@ -0,0 +1,11 @@ +from rich._stack import Stack + + +def test_stack(): + + stack = Stack() + stack.push("foo") + stack.push("bar") + assert stack.top == "bar" + assert stack.pop() == "bar" + assert stack.top == "foo" diff --git a/tests/test_status.py b/tests/test_status.py new file mode 100644 index 0000000..3abaa33 --- /dev/null +++ b/tests/test_status.py @@ -0,0 +1,31 @@ +from time import sleep + +from rich.console import Console +from rich.status import Status +from rich.table import Table + + +def test_status(): + + console = Console( + color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0 + ) + status = Status("foo", console=console) + assert status.console == console + status.update(status="bar", spinner="dots2", spinner_style="red", speed=2.0) + + assert isinstance(status.renderable, Table) + + # TODO: Testing output is tricky with threads + with status: + sleep(0.2) + + +def test_renderable(): + console = Console( + color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0 + ) + status = Status("foo", console=console) + console.begin_capture() + console.print(status) + assert console.end_capture() == "⠋ foo\n" diff --git a/tests/test_style.py b/tests/test_style.py new file mode 100644 index 0000000..8282331 --- /dev/null +++ b/tests/test_style.py @@ -0,0 +1,208 @@ +import pytest + +from rich.color import Color, ColorSystem, ColorType +from rich import errors +from rich.style import Style, StyleStack + + +def test_str(): + assert str(Style(bold=False)) == "not bold" + assert str(Style(color="red", bold=False)) == "not bold red" + assert str(Style(color="red", bold=False, italic=True)) == "not bold italic red" + assert str(Style()) == "none" + assert str(Style(bold=True)) == "bold" + assert str(Style(color="red", bold=True)) == "bold red" + assert str(Style(color="red", bgcolor="black", bold=True)) == "bold red on black" + all_styles = Style( + color="red", + bgcolor="black", + bold=True, + dim=True, + italic=True, + underline=True, + blink=True, + blink2=True, + reverse=True, + conceal=True, + strike=True, + underline2=True, + frame=True, + encircle=True, + overline=True, + ) + expected = "bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline red on black" + assert str(all_styles) == expected + assert str(Style(link="foo")) == "link foo" + + +def test_ansi_codes(): + all_styles = Style( + color="red", + bgcolor="black", + bold=True, + dim=True, + italic=True, + underline=True, + blink=True, + blink2=True, + reverse=True, + conceal=True, + strike=True, + underline2=True, + frame=True, + encircle=True, + overline=True, + ) + expected = "1;2;3;4;5;6;7;8;9;21;51;52;53;31;40" + assert all_styles._make_ansi_codes(ColorSystem.TRUECOLOR) == expected + + +def test_repr(): + assert repr(Style(bold=True, color="red")) == 'Style.parse("bold red")' + + +def test_eq(): + assert Style(bold=True, color="red") == Style(bold=True, color="red") + assert Style(bold=True, color="red") != Style(bold=True, color="green") + assert Style().__eq__("foo") == NotImplemented + + +def test_hash(): + assert isinstance(hash(Style()), int) + + +def test_empty(): + assert Style.null() == Style() + + +def test_bool(): + assert bool(Style()) is False + assert bool(Style(bold=True)) is True + assert bool(Style(color="red")) is True + assert bool(Style.parse("")) is False + + +def test_color_property(): + assert Style(color="red").color == Color("red", ColorType.STANDARD, 1, None) + + +def test_bgcolor_property(): + assert Style(bgcolor="black").bgcolor == Color("black", ColorType.STANDARD, 0, None) + + +def test_parse(): + assert Style.parse("") == Style() + assert Style.parse("red") == Style(color="red") + assert Style.parse("not bold") == Style(bold=False) + assert Style.parse("bold red on black") == Style( + color="red", bgcolor="black", bold=True + ) + assert Style.parse("bold link https://example.org") == Style( + bold=True, link="https://example.org" + ) + with pytest.raises(errors.StyleSyntaxError): + Style.parse("on") + with pytest.raises(errors.StyleSyntaxError): + Style.parse("on nothing") + with pytest.raises(errors.StyleSyntaxError): + Style.parse("rgb(999,999,999)") + with pytest.raises(errors.StyleSyntaxError): + Style.parse("not monkey") + with pytest.raises(errors.StyleSyntaxError): + Style.parse("link") + + +def test_link_id(): + assert Style().link_id == "" + assert Style.parse("").link_id == "" + assert Style.parse("red").link_id == "" + style = Style.parse("red link https://example.org") + assert isinstance(style.link_id, str) + assert len(style.link_id) > 1 + + +def test_get_html_style(): + expected = "color: #7f7fbf; background-color: #800000; font-weight: bold; font-style: italic; text-decoration: underline; text-decoration: line-through; text-decoration: overline" + assert ( + Style( + reverse=True, + dim=True, + color="red", + bgcolor="blue", + bold=True, + italic=True, + underline=True, + strike=True, + overline=True, + ).get_html_style() + == expected + ) + + +def test_chain(): + assert Style.chain(Style(color="red"), Style(bold=True)) == Style( + color="red", bold=True + ) + + +def test_copy(): + style = Style(color="red", bgcolor="black", italic=True) + assert style == style.copy() + assert style is not style.copy() + + +def test_render(): + assert Style(color="red").render("foo", color_system=None) == "foo" + assert ( + Style(color="red", bgcolor="black", bold=True).render("foo") + == "\x1b[1;31;40mfoo\x1b[0m" + ) + assert Style().render("foo") == "foo" + + +def test_test(): + Style(color="red").test("hello") + + +def test_add(): + assert Style(color="red") + None == Style(color="red") + assert Style().__add__("foo") == NotImplemented + + +def test_iadd(): + style = Style(color="red") + style += Style(bold=True) + assert style == Style(color="red", bold=True) + style += None + assert style == Style(color="red", bold=True) + + +def test_style_stack(): + stack = StyleStack(Style(color="red")) + repr(stack) + assert stack.current == Style(color="red") + stack.push(Style(bold=True)) + assert stack.current == Style(color="red", bold=True) + stack.pop() + assert stack.current == Style(color="red") + + +def test_pick_first(): + with pytest.raises(ValueError): + Style.pick_first() + + +def test_background_style(): + assert Style(bold=True, color="yellow", bgcolor="red").background_style == Style( + bgcolor="red" + ) + + +def test_without_color(): + style = Style(bold=True, color="red", bgcolor="blue") + colorless_style = style.without_color + assert colorless_style.color == None + assert colorless_style.bgcolor == None + assert colorless_style.bold == True + null_style = Style.null() + assert null_style.without_color == null_style diff --git a/tests/test_styled.py b/tests/test_styled.py new file mode 100644 index 0000000..8ebe5e8 --- /dev/null +++ b/tests/test_styled.py @@ -0,0 +1,15 @@ +import io + +from rich.console import Console +from rich.measure import Measurement +from rich.styled import Styled + + +def test_styled(): + styled_foo = Styled("foo", "on red") + console = Console(file=io.StringIO(), force_terminal=True) + assert Measurement.get(console, styled_foo, 80) == Measurement(3, 3) + console.print(styled_foo) + result = console.file.getvalue() + expected = "\x1b[41mfoo\x1b[0m\n" + assert result == expected diff --git a/tests/test_syntax.py b/tests/test_syntax.py new file mode 100644 index 0000000..e7575a6 --- /dev/null +++ b/tests/test_syntax.py @@ -0,0 +1,221 @@ +# coding=utf-8 + +import sys +import os, tempfile + +import pytest +from .render import render + +from rich.panel import Panel +from rich.style import Style +from rich.syntax import ( + Syntax, + ANSISyntaxTheme, + PygmentsSyntaxTheme, + Color, + Console, + ConsoleOptions, +) + + +CODE = ''' +def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: + """Iterate and generate a tuple with a flag for first and last value.""" + iter_values = iter(values) + try: + previous_value = next(iter_values) + except StopIteration: + return + first = True + for value in iter_values: + yield first, False, previous_value + first = False + previous_value = value + yield first, True, previous_value +''' + + +def test_python_render(): + syntax = Panel.fit( + Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme="foo", + code_width=60, + word_wrap=True, + ), + padding=0, + ) + rendered_syntax = render(syntax) + print(repr(rendered_syntax)) + expected = '╭────────────────────────────────────────────────────────────────╮\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 2 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248m"""Iterate and generate a tuple with a flag for first \x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[48;2;248;248;248m \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248mand last value."""\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 3 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248miter\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalues\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 4 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mtry\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 5 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248mnext\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 6 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mexcept\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;210;65;58;48;2;248;248;248mStopIteration\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 7 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mreturn\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 8 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mTrue\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 9 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mfor\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalue\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;170;34;255;48;2;248;248;248min\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m10 \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248myield\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mFalse\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n╰────────────────────────────────────────────────────────────────╯\n' + assert rendered_syntax == expected + + +def test_python_render_indent_guides(): + syntax = Panel.fit( + Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme="foo", + code_width=60, + word_wrap=True, + indent_guides=True, + ), + padding=0, + ) + rendered_syntax = render(syntax) + print(repr(rendered_syntax)) + expected = '╭────────────────────────────────────────────────────────────────╮\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 2 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248m"""Iterate and generate a tuple with a flag for first \x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[48;2;248;248;248m \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248mand last value."""\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 3 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248miter\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalues\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 4 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mtry\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 5 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248mnext\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 6 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mexcept\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;210;65;58;48;2;248;248;248mStopIteration\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 7 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mreturn\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 8 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mTrue\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 9 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mfor\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalue\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;170;34;255;48;2;248;248;248min\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n│\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m10 \x1b[0m\x1b[2;3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248myield\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mFalse\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n╰────────────────────────────────────────────────────────────────╯\n' + assert rendered_syntax == expected + + +def test_pygments_syntax_theme_non_str(): + from pygments.style import Style as PygmentsStyle + + style = PygmentsSyntaxTheme(PygmentsStyle()) + assert style.get_background_style().bgcolor == Color.parse("#ffffff") + + +def test_pygments_syntax_theme(): + style = PygmentsSyntaxTheme("default") + assert style.get_style_for_token("abc") == Style.parse("none") + + +def test_get_line_color_none(): + style = PygmentsSyntaxTheme("default") + style._background_style = Style(bgcolor=None) + syntax = Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme=style, + code_width=60, + word_wrap=True, + background_color="red", + ) + assert syntax._get_line_numbers_color() == Color.default() + + +def test_highlight_background_color(): + syntax = Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme="foo", + code_width=60, + word_wrap=True, + background_color="red", + ) + assert syntax.highlight(CODE).style == Style.parse("on red") + + +def test_get_number_styles(): + syntax = Syntax(CODE, "python", theme="monokai", line_numbers=True) + console = Console(color_system="windows") + assert syntax._get_number_styles(console=console) == ( + Style.parse("on #272822"), + Style.parse("dim on #272822"), + Style.parse("not dim on #272822"), + ) + + +def test_get_style_for_token(): + # from pygments.style import Style as PygmentsStyle + # pygments_style = PygmentsStyle() + from pygments.style import Token + + style = PygmentsSyntaxTheme("default") + style_dict = {Token.Text: Style(color=None)} + style._style_cache = style_dict + syntax = Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme=style, + code_width=60, + word_wrap=True, + background_color="red", + ) + assert syntax._get_line_numbers_color() == Color.default() + + +def test_option_no_wrap(): + + from rich.console import Console + + console = Console + + syntax = Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + code_width=60, + word_wrap=False, + background_color="red", + ) + + rendered_syntax = render(syntax, True) + # print(repr(rendered_syntax)) + + expected = '\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 2 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;230;219;116;41m"""Iterate and generate a tuple with a flag for first and last value."""\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 3 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41miter_values\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;249;38;114;41m=\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41miter\x1b[0m\x1b[38;2;248;248;242;41m(\x1b[0m\x1b[38;2;248;248;242;41mvalues\x1b[0m\x1b[38;2;248;248;242;41m)\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 4 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mtry\x1b[0m\x1b[38;2;248;248;242;41m:\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 5 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mprevious_value\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;249;38;114;41m=\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mnext\x1b[0m\x1b[38;2;248;248;242;41m(\x1b[0m\x1b[38;2;248;248;242;41miter_values\x1b[0m\x1b[38;2;248;248;242;41m)\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 6 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mexcept\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;166;226;46;41mStopIteration\x1b[0m\x1b[38;2;248;248;242;41m:\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 7 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mreturn\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 8 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mfirst\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;249;38;114;41m=\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mTrue\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m 9 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mfor\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mvalue\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;249;38;114;41min\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41miter_values\x1b[0m\x1b[38;2;248;248;242;41m:\x1b[0m\n\x1b[1;38;2;227;227;221;48;2;39;40;34m \x1b[0m\x1b[38;2;101;102;96;48;2;39;40;34m10 \x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41myield\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mfirst\x1b[0m\x1b[38;2;248;248;242;41m,\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;102;217;239;41mFalse\x1b[0m\x1b[38;2;248;248;242;41m,\x1b[0m\x1b[38;2;248;248;242;41m \x1b[0m\x1b[38;2;248;248;242;41mprevious_value\x1b[0m\n' + # console.print(syntax, no_wrap=True) + + assert rendered_syntax == expected + + +def test_ansi_theme(): + style = Style(color="red") + theme = ANSISyntaxTheme({("foo", "bar"): style}) + assert theme.get_style_for_token(("foo", "bar", "baz")) == style + assert theme.get_background_style() == Style() + + +@pytest.mark.skipif(sys.platform == "win32", reason="permissions error on Windows") +def test_from_file(): + fh, path = tempfile.mkstemp("example.py") + try: + os.write(fh, b"import this\n") + syntax = Syntax.from_path(path) + assert syntax.lexer_name == "Python" + assert syntax.code == "import this\n" + finally: + os.remove(path) + + +@pytest.mark.skipif(sys.platform == "win32", reason="permissions error on Windows") +def test_from_file_unknown_lexer(): + fh, path = tempfile.mkstemp("example.nosuchtype") + try: + os.write(fh, b"import this\n") + syntax = Syntax.from_path(path) + assert syntax.lexer_name == "default" + assert syntax.code == "import this\n" + finally: + os.remove(path) + + +if __name__ == "__main__": + syntax = Panel.fit( + Syntax( + CODE, + lexer_name="python", + line_numbers=True, + line_range=(2, 10), + theme="foo", + code_width=60, + word_wrap=True, + ), + padding=0, + ) + rendered = render(markdown) + print(rendered) + print(repr(rendered)) diff --git a/tests/test_table.py b/tests/test_table.py new file mode 100644 index 0000000..7a57167 --- /dev/null +++ b/tests/test_table.py @@ -0,0 +1,142 @@ +# encoding=utf-8 + +import io +from rich import color + +import pytest + +from rich import errors +from rich.console import Console +from rich.measure import Measurement +from rich.table import Table, Column +from rich.text import Text + + +def render_tables(): + console = Console( + width=60, + force_terminal=True, + file=io.StringIO(), + legacy_windows=False, + color_system=None, + ) + + table = Table(title="test table", caption="table caption", expand=True) + table.add_column("foo", footer=Text("total"), no_wrap=True, overflow="ellipsis") + table.add_column("bar", justify="center") + table.add_column("baz", justify="right") + + table.add_row("Averlongwordgoeshere", "banana pancakes", None) + + assert Measurement.get(console, table, 80) == Measurement(41, 48) + + for width in range(10, 60, 5): + console.print(table, width=width) + + table.expand = False + console.print(table, justify="left") + console.print(table, justify="center") + console.print(table, justify="right") + + assert table.row_count == 1 + + table.row_styles = ["red", "yellow"] + table.add_row("Coffee") + table.add_row("Coffee", "Chocolate", None, "cinnamon") + + assert table.row_count == 3 + + console.print(table) + + table.show_lines = True + console.print(table) + + table.show_footer = True + console.print(table) + + table.show_edge = False + + console.print(table) + + table.padding = 1 + console.print(table) + + table.width = 20 + assert Measurement.get(console, table, 80) == Measurement(20, 20) + console.print(table) + + table.columns[0].no_wrap = True + table.columns[1].no_wrap = True + table.columns[2].no_wrap = True + + console.print(table) + + table.padding = 0 + table.width = 60 + table.leading = 1 + console.print(table) + + return console.file.getvalue() + + +def test_render_table(): + expected = " test table \n┏━━━━━━┳━┳━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━╇━╇━┩\n│ Ave… │ │ │\n└──────┴─┴─┘\n table \n caption \n test table \n┏━━━━━━━━━━━┳━┳━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━╇━╇━┩\n│ Averlong… │ │ │\n└───────────┴─┴─┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━┳━┳━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━━━━━━╇━╇━┩\n│ Averlongwordg… │ │ │\n└────────────────┴─┴─┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━┳━┳━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━╇━╇━┩\n│ Averlongwordgoeshe… │ │ │\n└─────────────────────┴─┴─┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━┳━━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━╇━━┩\n│ Averlongwordgoeshere │ │ │\n└──────────────────────┴──┴──┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━┩\n│ Averlongwordgoeshere │ ba… │ │\n│ │ pa… │ │\n└──────────────────────┴─────┴────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancak… │ │\n└──────────────────────┴─────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancakes │ │\n└──────────────────────┴──────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└───────────────────────┴──────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└──────────────────────────┴────────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n┃ foo ┃ bar ┃ baz ┃ \n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n│ Averlongwordgoeshere │ banana pancakes │ │ \n└──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n ┃ foo ┃ bar ┃ baz ┃ \n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n │ Averlongwordgoeshere │ banana pancakes │ │ \n └──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓\n ┃ foo ┃ bar ┃ baz ┃\n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩\n │ Averlongwordgoeshere │ banana pancakes │ │\n └──────────────────────┴─────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n│ Coffee │ │ │ │\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ total │ │ │ │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n foo ┃ bar ┃ baz ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n Averlongwordgoeshere │ banana pancakes │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ Chocolate │ │ cinnamon \n──────────────────────┼─────────────────┼─────┼──────────\n total │ │ │ \n table caption \n test table \n ┃ ┃ ┃ \n foo ┃ bar ┃ baz ┃ \n ┃ ┃ ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n │ │ │ \n Averlongwordgoeshere │ banana pancakes │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ Chocolate │ │ cinnamon \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n total │ │ │ \n │ │ │ \n table caption \n test table \n ┃ ┃ ┃ \n foo ┃ ┃ ┃ \n ┃ ┃ ┃ \n━━━━━━━━━━━━━━━━━╇━╇━╇━\n │ │ │ \n Averlongwordgo… │ │ │ \n │ │ │ \n─────────────────┼─┼─┼─\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n─────────────────┼─┼─┼─\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n─────────────────┼─┼─┼─\n │ │ │ \n total │ │ │ \n │ │ │ \n table caption \n test table \n ┃ ┃ ┃ \n foo ┃ bar ┃ ┃ \n ┃ ┃ ┃ \n━━━━━━━━━━╇━━━━━━━━━╇━╇━\n │ │ │ \n Averlon… │ banana… │ │ \n │ │ │ \n──────────┼─────────┼─┼─\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n──────────┼─────────┼─┼─\n │ │ │ \n Coffee │ Chocol… │ │ \n │ │ │ \n──────────┼─────────┼─┼─\n │ │ │ \n total │ │ │ \n │ │ │ \n table caption \n test table \nfoo ┃ bar ┃ baz┃ \n━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━╇━━━━━━━━━\nAverlongwordgoeshere │ banana pancakes │ │ \n │ │ │ \nCoffee │ │ │ \n │ │ │ \nCoffee │ Chocolate │ │cinnamon \n─────────────────────────┼───────────────────┼────┼─────────\ntotal │ │ │ \n table caption \n" + assert render_tables() == expected + + +def test_not_renderable(): + class Foo: + pass + + table = Table() + with pytest.raises(errors.NotRenderableError): + table.add_row(Foo()) + + +def test_init_append_column(): + header_names = ["header1", "header2", "header3"] + test_columns = [ + Column(_index=index, header=header) for index, header in enumerate(header_names) + ] + + # Test appending of strings for header names + assert Table(*header_names).columns == test_columns + # Test directly passing a Table Column objects + assert Table(*test_columns).columns == test_columns + + +def test_rich_measure(): + # Check __rich_measure__() for a negative width passed as an argument + assert Table("test_header", width=None).__rich_measure__( + Console(), -1 + ) == Measurement(0, 0) + # Check __rich_measure__() for a negative Table.width attribute + assert Table("test_header", width=-1).__rich_measure__(Console(), 1) == Measurement( + 0, 0 + ) + # Check __rich_measure__() for a positive width passed as an argument + assert Table("test_header", width=None).__rich_measure__( + Console(), 10 + ) == Measurement(10, 10) + # Check __rich_measure__() for a positive Table.width attribute + assert Table("test_header", width=10).__rich_measure__( + Console(), -1 + ) == Measurement(10, 10) + + +def test_min_width(): + table = Table("foo", min_width=30) + table.add_row("bar") + assert table.__rich_measure__(Console(), 100) == Measurement(30, 30) + console = Console(color_system=None) + console.begin_capture() + console.print(table) + output = console.end_capture() + print(output) + assert all(len(line) == 30 for line in output.splitlines()) + + +if __name__ == "__main__": + render = render_tables() + print(render) + print(repr(render)) diff --git a/tests/test_tabulate.py b/tests/test_tabulate.py new file mode 100644 index 0000000..37e86bf --- /dev/null +++ b/tests/test_tabulate.py @@ -0,0 +1,34 @@ +import itertools +from rich.style import Style +from rich.table import _Cell +from rich.tabulate import tabulate_mapping + + +def test_tabulate_mapping(): + # TODO: tabulate_mapping may not be needed shortly + table = tabulate_mapping({"foo": "1", "bar": "2"}) + assert len(table.columns) == 2 + assert len(table.columns[0]._cells) == 2 + assert len(table.columns[1]._cells) == 2 + + # add tests for title and caption justification + test_title = "Foo v. Bar" + test_caption = "approximate results" + for title_justify, caption_justify in itertools.product( + [None, "left", "center", "right"], repeat=2 + ): + table = tabulate_mapping( + {"foo": "1", "bar": "2"}, + title=test_title, + caption=test_caption, + title_justify=title_justify, + caption_justify=caption_justify, + ) + expected_title_justify = ( + title_justify if title_justify is not None else "center" + ) + expected_caption_justify = ( + caption_justify if caption_justify is not None else "center" + ) + assert expected_title_justify == table.title_justify + assert expected_caption_justify == table.caption_justify diff --git a/tests/test_text.py b/tests/test_text.py new file mode 100644 index 0000000..3bf6888 --- /dev/null +++ b/tests/test_text.py @@ -0,0 +1,677 @@ +from io import StringIO +import pytest + +from rich.console import Console +from rich.text import Span, Text +from rich.measure import Measurement +from rich.style import Style + + +def test_span(): + span = Span(1, 10, "foo") + repr(span) + assert bool(span) + assert not Span(10, 10, "foo") + + +def test_span_split(): + assert Span(5, 10, "foo").split(2) == (Span(5, 10, "foo"), None) + assert Span(5, 10, "foo").split(15) == (Span(5, 10, "foo"), None) + assert Span(0, 10, "foo").split(5) == (Span(0, 5, "foo"), Span(5, 10, "foo")) + + +def test_span_move(): + assert Span(5, 10, "foo").move(2) == Span(7, 12, "foo") + + +def test_span_right_crop(): + assert Span(5, 10, "foo").right_crop(15) == Span(5, 10, "foo") + assert Span(5, 10, "foo").right_crop(7) == Span(5, 7, "foo") + + +def test_len(): + assert len(Text("foo")) == 3 + + +def test_cell_len(): + assert Text("foo").cell_len == 3 + assert Text("😀").cell_len == 2 + + +def test_bool(): + assert Text("foo") + assert not Text("") + + +def test_str(): + assert str(Text("foo")) == "foo" + + +def test_repr(): + assert isinstance(repr(Text("foo")), str) + + +def test_add(): + text = Text("foo") + Text("bar") + assert str(text) == "foobar" + assert Text("foo").__add__(1) == NotImplemented + + +def test_eq(): + assert Text("foo") == Text("foo") + assert Text("foo") != Text("bar") + assert Text("foo").__eq__(1) == NotImplemented + + +def test_contain(): + test = Text("foobar") + assert "foo" in test + assert "foo " not in test + assert Text("bar") in test + assert None not in test + + +def test_plain_property(): + text = Text("foo") + text.append("bar") + text.append("baz") + assert text.plain == "foobarbaz" + + +def test_plain_property_setter(): + test = Text("foo") + test.plain = "bar" + assert str(test) == "bar" + test = Text() + test.append("Hello, World", "bold") + test.plain = "Hello" + assert str(test) == "Hello" + assert test._spans == [Span(0, 5, "bold")] + + +def test_from_markup(): + text = Text.from_markup("Hello, [bold]World![/bold]") + assert str(text) == "Hello, World!" + assert text._spans == [Span(7, 13, "bold")] + + +def test_copy(): + test = Text() + test.append("Hello", "bold") + test.append(" ") + test.append("World", "italic") + test_copy = test.copy() + assert test == test_copy + assert test is not test_copy + + +def test_rstrip(): + test = Text("Hello, World! ") + test.rstrip() + assert str(test) == "Hello, World!" + + +def test_rstrip_end(): + test = Text("Hello, World! ") + test.rstrip_end(14) + assert str(test) == "Hello, World! " + + +def test_stylize(): + test = Text("Hello, World!") + test.stylize("bold", 7, 11) + assert test._spans == [Span(7, 11, "bold")] + test.stylize("bold", 20, 25) + assert test._spans == [Span(7, 11, "bold")] + + +def test_stylize_negative_index(): + test = Text("Hello, World!") + test.stylize("bold", -6, -1) + assert test._spans == [Span(7, 12, "bold")] + + +def test_highlight_regex(): + test = Text("peek-a-boo") + + count = test.highlight_regex(r"NEVER_MATCH", "red") + assert count == 0 + assert len(test._spans) == 0 + + # text: peek-a-boo + # indx: 0123456789 + count = test.highlight_regex(r"[a|e|o]+", "red") + assert count == 3 + assert sorted(test._spans) == [ + Span(1, 3, "red"), + Span(5, 6, "red"), + Span(8, 10, "red"), + ] + + test = Text("Ada Lovelace, Alan Turing") + count = test.highlight_regex( + r"(?P<yellow>[A-Za-z]+)[ ]+(?P<red>[A-Za-z]+)(?P<NEVER_MATCH>NEVER_MATCH)*" + ) + + # The number of matched name should be 2 + assert count == 2 + assert sorted(test._spans) == [ + Span(0, 3, "yellow"), # Ada + Span(4, 12, "red"), # Lovelace + Span(14, 18, "yellow"), # Alan + Span(19, 25, "red"), # Turing + ] + + +def test_highlight_regex_callable(): + test = Text("Vulnerability CVE-2018-6543 detected") + re_cve = r"CVE-\d{4}-\d+" + + def get_style(text: str) -> Style: + return Style.parse( + f"bold yellow link https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword={text}" + ) + + count = test.highlight_regex(re_cve, get_style) + assert count == 1 + assert len(test._spans) == 1 + assert test._spans[0].start == 14 + assert test._spans[0].end == 27 + assert ( + test._spans[0].style.link + == "https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2018-6543" + ) + + +def test_highlight_words(): + test = Text("Do NOT! touch anything!") + words = ["NOT", "!"] + count = test.highlight_words(words, "red") + assert count == 3 + assert sorted(test._spans) == [ + Span(3, 6, "red"), # NOT + Span(6, 7, "red"), # ! + Span(22, 23, "red"), # ! + ] + + # regex escape test + test = Text("[o|u]aeiou") + words = ["[a|e|i]", "[o|u]"] + count = test.highlight_words(words, "red") + assert count == 1 + assert test._spans == [Span(0, 5, "red")] + + # case sensitive + test = Text("AB Ab aB ab") + words = ["AB"] + + count = test.highlight_words(words, "red") + assert count == 1 + assert test._spans == [Span(0, 2, "red")] + + test = Text("AB Ab aB ab") + count = test.highlight_words(words, "red", case_sensitive=False) + assert count == 4 + + +def test_set_length(): + test = Text("Hello") + test.set_length(5) + assert test == Text("Hello") + + test = Text("Hello") + test.set_length(10) + assert test == Text("Hello ") + + test = Text("Hello World") + test.stylize("bold", 0, 5) + test.stylize("italic", 7, 9) + + test.set_length(3) + expected = Text() + expected.append("Hel", "bold") + assert test == expected + + +def test_console_width(): + console = Console() + test = Text("Hello World!\nfoobarbaz") + assert test.__rich_measure__(console, 80) == Measurement(9, 12) + assert Text(" " * 4).__rich_measure__(console, 80) == Measurement(4, 4) + + +def test_join(): + test = Text("bar").join([Text("foo", "red"), Text("baz", "blue")]) + assert str(test) == "foobarbaz" + assert test._spans == [Span(0, 3, "red"), Span(3, 6, ""), Span(6, 9, "blue")] + + +def test_trim_spans(): + test = Text("Hello") + test._spans[:] = [Span(0, 3, "red"), Span(3, 6, "green"), Span(6, 9, "blue")] + test._trim_spans() + assert test._spans == [Span(0, 3, "red"), Span(3, 5, "green")] + + +def test_pad_left(): + test = Text("foo") + test.pad_left(3, "X") + assert str(test) == "XXXfoo" + + +def test_pad_right(): + test = Text("foo") + test.pad_right(3, "X") + assert str(test) == "fooXXX" + + +def test_append(): + test = Text("foo") + test.append("bar") + assert str(test) == "foobar" + test.append(Text("baz", "bold")) + assert str(test) == "foobarbaz" + assert test._spans == [Span(6, 9, "bold")] + + with pytest.raises(ValueError): + test.append(Text("foo"), "bar") + + with pytest.raises(TypeError): + test.append(1) + + +def test_append_text(): + test = Text("foo") + test.append_text(Text("bar", style="bold")) + assert str(test) == "foobar" + assert test._spans == [Span(3, 6, "bold")] + + +def test_split(): + test = Text() + test.append("foo", "red") + test.append("\n") + test.append("bar", "green") + test.append("\n") + + line1 = Text() + line1.append("foo", "red") + line2 = Text() + line2.append("bar", "green") + split = test.split("\n") + assert len(split) == 2 + assert split[0] == line1 + assert split[1] == line2 + + assert list(Text("foo").split("\n")) == [Text("foo")] + + +def test_split_spans(): + test = Text.from_markup("[red]Hello\n[b]World") + lines = test.split("\n") + assert lines[0].plain == "Hello" + assert lines[1].plain == "World" + assert lines[0].spans == [Span(0, 5, "red")] + assert lines[1].spans == [Span(0, 5, "red"), Span(0, 5, "bold")] + + +def test_divide(): + lines = Text("foo").divide([]) + assert len(lines) == 1 + assert lines[0] == Text("foo") + + text = Text() + text.append("foo", "bold") + lines = text.divide([1, 2]) + assert len(lines) == 3 + assert str(lines[0]) == "f" + assert str(lines[1]) == "o" + assert str(lines[2]) == "o" + assert lines[0]._spans == [Span(0, 1, "bold")] + assert lines[1]._spans == [Span(0, 1, "bold")] + assert lines[2]._spans == [Span(0, 1, "bold")] + + text = Text() + text.append("foo", "red") + text.append("bar", "green") + text.append("baz", "blue") + lines = text.divide([8]) + assert len(lines) == 2 + assert str(lines[0]) == "foobarba" + assert str(lines[1]) == "z" + assert lines[0]._spans == [ + Span(0, 3, "red"), + Span(3, 6, "green"), + Span(6, 8, "blue"), + ] + assert lines[1]._spans == [Span(0, 1, "blue")] + + lines = text.divide([1]) + assert len(lines) == 2 + assert str(lines[0]) == "f" + assert str(lines[1]) == "oobarbaz" + assert lines[0]._spans == [Span(0, 1, "red")] + assert lines[1]._spans == [ + Span(0, 2, "red"), + Span(2, 5, "green"), + Span(5, 8, "blue"), + ] + + +def test_right_crop(): + test = Text() + test.append("foobar", "red") + test.right_crop(3) + assert str(test) == "foo" + assert test._spans == [Span(0, 3, "red")] + + +def test_wrap_3(): + test = Text("foo bar baz") + lines = test.wrap(Console(), 3) + print(repr(lines)) + assert len(lines) == 3 + assert lines[0] == Text("foo") + assert lines[1] == Text("bar") + assert lines[2] == Text("baz") + + +def test_wrap_4(): + test = Text("foo bar baz", justify="left") + lines = test.wrap(Console(), 4) + assert len(lines) == 3 + assert lines[0] == Text("foo ") + assert lines[1] == Text("bar ") + assert lines[2] == Text("baz ") + + +def test_wrap_long(): + test = Text("abracadabra", justify="left") + lines = test.wrap(Console(), 4) + assert len(lines) == 3 + assert lines[0] == Text("abra") + assert lines[1] == Text("cada") + assert lines[2] == Text("bra ") + + +def test_wrap_overflow(): + test = Text("Some more words") + lines = test.wrap(Console(), 4, overflow="ellipsis") + assert (len(lines)) == 3 + assert lines[0] == Text("Some") + assert lines[1] == Text("more") + assert lines[2] == Text("wor…") + + +def test_wrap_overflow_long(): + test = Text("bigword" * 10) + lines = test.wrap(Console(), 4, overflow="ellipsis") + assert len(lines) == 1 + assert lines[0] == Text("big…") + + +def test_wrap_long_words(): + test = Text("X 123456789", justify="left") + lines = test.wrap(Console(), 4) + + assert len(lines) == 3 + assert lines[0] == Text("X 12") + assert lines[1] == Text("3456") + assert lines[2] == Text("789 ") + + +def test_no_wrap_no_crop(): + test = Text("Hello World!" * 3) + + console = Console(width=20, file=StringIO()) + console.print(test, no_wrap=True) + console.print(test, no_wrap=True, crop=False, overflow="ignore") + + print(repr(console.file.getvalue())) + assert ( + console.file.getvalue() + == "Hello World!Hello Wo\nHello World!Hello World!Hello World!\n" + ) + + +def test_fit(): + test = Text("Hello\nWorld") + lines = test.fit(3) + assert str(lines[0]) == "Hel" + assert str(lines[1]) == "Wor" + + +def test_wrap_tabs(): + test = Text("foo\tbar", justify="left") + lines = test.wrap(Console(), 4) + assert len(lines) == 2 + assert str(lines[0]) == "foo " + assert str(lines[1]) == "bar " + + +def test_render(): + console = Console(width=15, record=True) + test = Text.from_markup( + "[u][b]Where[/b] there is a [i]Will[/i], there is a Way.[/u]" + ) + console.print(test) + output = console.export_text(styles=True) + expected = "\x1b[1;4mWhere\x1b[0m\x1b[4m there is \x1b[0m\n\x1b[4ma \x1b[0m\x1b[3;4mWill\x1b[0m\x1b[4m, there \x1b[0m\n\x1b[4mis a Way.\x1b[0m\n" + assert output == expected + + +def test_render_simple(): + console = Console(width=80) + console.begin_capture() + console.print(Text("foo")) + result = console.end_capture() + assert result == "foo\n" + + +@pytest.mark.parametrize( + "print_text,result", + [ + (("."), ".\n"), + ((".", "."), ". .\n"), + (("Hello", "World", "!"), "Hello World !\n"), + ], +) +def test_print(print_text, result): + console = Console(record=True) + console.print(*print_text) + assert console.export_text(styles=False) == result + + +@pytest.mark.parametrize( + "print_text,result", + [ + (("."), ".X"), + ((".", "."), "..X"), + (("Hello", "World", "!"), "HelloWorld!X"), + ], +) +def test_print_sep_end(print_text, result): + console = Console(record=True, file=StringIO()) + console.print(*print_text, sep="", end="X") + assert console.file.getvalue() == result + + +def test_tabs_to_spaces(): + test = Text("\tHello\tWorld", tab_size=8) + test.expand_tabs() + assert test.plain == " Hello World" + + test = Text("\tHello\tWorld", tab_size=4) + test.expand_tabs() + assert test.plain == " Hello World" + + test = Text(".\t..\t...\t....\t", tab_size=4) + test.expand_tabs() + assert test.plain == ". .. ... .... " + + test = Text("No Tabs") + test.expand_tabs() + assert test.plain == "No Tabs" + + +def test_markup_switch(): + """Test markup can be disabled.""" + console = Console(file=StringIO(), markup=False) + console.print("[bold]foo[/bold]") + assert console.file.getvalue() == "[bold]foo[/bold]\n" + + +def test_emoji(): + """Test printing emoji codes.""" + console = Console(file=StringIO()) + console.print(":+1:") + assert console.file.getvalue() == "👍\n" + + +def test_emoji_switch(): + """Test emoji can be disabled.""" + console = Console(file=StringIO(), emoji=False) + console.print(":+1:") + assert console.file.getvalue() == ":+1:\n" + + +def test_assemble(): + text = Text.assemble("foo", ("bar", "bold")) + assert str(text) == "foobar" + assert text._spans == [Span(3, 6, "bold")] + + +def test_styled(): + text = Text.styled("foo", "bold red") + assert text.style == "" + assert str(text) == "foo" + assert text._spans == [Span(0, 3, "bold red")] + + +def test_strip_control_codes(): + text = Text("foo\rbar") + assert str(text) == "foobar" + text.append("\x08") + assert str(text) == "foobar" + + +def test_get_style_at_offset(): + console = Console() + text = Text.from_markup("Hello [b]World[/b]") + assert text.get_style_at_offset(console, 0) == Style() + assert text.get_style_at_offset(console, 6) == Style(bold=True) + + +@pytest.mark.parametrize( + "input, count, expected", + [ + ("Hello", 10, "Hello"), + ("Hello", 5, "Hello"), + ("Hello", 4, "Hel…"), + ("Hello", 3, "He…"), + ("Hello", 2, "H…"), + ("Hello", 1, "…"), + ], +) +def test_truncate_ellipsis(input, count, expected): + text = Text(input) + text.truncate(count, overflow="ellipsis") + assert text.plain == expected + + +@pytest.mark.parametrize( + "input, count, expected", + [ + ("Hello", 5, "Hello"), + ("Hello", 10, "Hello "), + ("Hello", 3, "He…"), + ], +) +def test_truncate_ellipsis_pad(input, count, expected): + text = Text(input) + text.truncate(count, overflow="ellipsis", pad=True) + assert text.plain == expected + + +def test_pad(): + test = Text("foo") + test.pad(2) + assert test.plain == " foo " + + +def test_align_left(): + test = Text("foo") + test.align("left", 10) + assert test.plain == "foo " + + +def test_align_right(): + test = Text("foo") + test.align("right", 10) + assert test.plain == " foo" + + +def test_align_center(): + test = Text("foo") + test.align("center", 10) + assert test.plain == " foo " + + +def test_detect_indentation(): + test = """\ +foo + bar + """ + assert Text(test).detect_indentation() == 4 + test = """\ +foo + bar + baz + """ + assert Text(test).detect_indentation() == 2 + assert Text("").detect_indentation() == 1 + assert Text(" ").detect_indentation() == 1 + + +def test_indentation_guides(): + test = Text( + """\ +for a in range(10): + print(a) + +foo = [ + 1, + { + 2 + } +] + +""" + ) + result = test.with_indent_guides() + print(result.plain) + print(repr(result.plain)) + expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n│ {\n│ │ 2\n│ }\n]\n" + assert result.plain == expected + + +def test_slice(): + + text = Text.from_markup("[red]foo [bold]bar[/red] baz[/bold]") + assert text[0] == Text("f", spans=[Span(0, 1, "red")]) + assert text[4] == Text("b", spans=[Span(0, 1, "red"), Span(0, 1, "bold")]) + + assert text[:3] == Text("foo", spans=[Span(0, 3, "red")]) + assert text[:4] == Text("foo ", spans=[Span(0, 4, "red")]) + assert text[:5] == Text("foo b", spans=[Span(0, 5, "red"), Span(4, 5, "bold")]) + assert text[4:] == Text("bar baz", spans=[Span(0, 3, "red"), Span(0, 7, "bold")]) + + with pytest.raises(TypeError): + text[::-1] + + +def test_wrap_invalid_style(): + # https://github.com/willmcgugan/rich/issues/987 + console = Console(width=100, color_system="truecolor") + a = "[#######.................] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [#######.................]" + console.print(a, justify="full") diff --git a/tests/test_theme.py b/tests/test_theme.py new file mode 100644 index 0000000..228cb18 --- /dev/null +++ b/tests/test_theme.py @@ -0,0 +1,53 @@ +import io +import os +import tempfile + +import pytest + +from rich.style import Style +from rich.theme import Theme, ThemeStack, ThemeStackError + + +def test_inherit(): + theme = Theme({"warning": "red"}) + assert theme.styles["warning"] == Style(color="red") + assert theme.styles["dim"] == Style(dim=True) + + +def test_config(): + theme = Theme({"warning": "red"}) + config = theme.config + assert "warning = red\n" in config + + +def test_from_file(): + theme = Theme({"warning": "red"}) + text_file = io.StringIO() + text_file.write(theme.config) + text_file.seek(0) + + load_theme = Theme.from_file(text_file) + assert theme.styles == load_theme.styles + + +def test_read(): + theme = Theme({"warning": "red"}) + with tempfile.TemporaryDirectory("richtheme") as name: + filename = os.path.join(name, "theme.cfg") + with open(filename, "wt") as write_theme: + write_theme.write(theme.config) + load_theme = Theme.read(filename) + assert theme.styles == load_theme.styles + + +def test_theme_stack(): + theme = Theme({"warning": "red"}) + stack = ThemeStack(theme) + assert stack.get("warning") == Style.parse("red") + new_theme = Theme({"warning": "bold yellow"}) + stack.push_theme(new_theme) + assert stack.get("warning") == Style.parse("bold yellow") + stack.pop_theme() + assert stack.get("warning") == Style.parse("red") + with pytest.raises(ThemeStackError): + stack.pop_theme() diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..2e6fef7 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,38 @@ +from rich._loop import loop_first, loop_last, loop_first_last +from rich._ratio import ratio_distribute + + +def test_loop_first(): + assert list(loop_first([])) == [] + iterable = loop_first(["apples", "oranges", "pears", "lemons"]) + assert next(iterable) == (True, "apples") + assert next(iterable) == (False, "oranges") + assert next(iterable) == (False, "pears") + assert next(iterable) == (False, "lemons") + + +def test_loop_last(): + assert list(loop_last([])) == [] + iterable = loop_last(["apples", "oranges", "pears", "lemons"]) + assert next(iterable) == (False, "apples") + assert next(iterable) == (False, "oranges") + assert next(iterable) == (False, "pears") + assert next(iterable) == (True, "lemons") + + +def test_loop_first_last(): + assert list(loop_first_last([])) == [] + iterable = loop_first_last(["apples", "oranges", "pears", "lemons"]) + assert next(iterable) == (True, False, "apples") + assert next(iterable) == (False, False, "oranges") + assert next(iterable) == (False, False, "pears") + assert next(iterable) == (False, True, "lemons") + + +def test_ratio_distribute(): + assert ratio_distribute(10, [1]) == [10] + assert ratio_distribute(10, [1, 1]) == [5, 5] + assert ratio_distribute(12, [1, 3]) == [3, 9] + assert ratio_distribute(0, [1, 3]) == [0, 0] + assert ratio_distribute(0, [1, 3], [1, 1]) == [1, 1] + assert ratio_distribute(10, [1, 0]) == [10, 0] diff --git a/tests/test_traceback.py b/tests/test_traceback.py new file mode 100644 index 0000000..6941a2e --- /dev/null +++ b/tests/test_traceback.py @@ -0,0 +1,211 @@ +import io +import sys + +import pytest + +from rich.console import Console +from rich.traceback import install, Traceback + +# from .render import render + +try: + from ._exception_render import expected +except ImportError: + expected = None + + +CAPTURED_EXCEPTION = 'Traceback (most recent call last):\n╭──────────────────────────────────────────────────────────────────────────────────────────────────╮\n│ File "/Users/willmcgugan/projects/rich/tests/test_traceback.py", line 26, in test_handler │\n│ 23 try: │\n│ 24 old_handler = install(console=console, line_numbers=False) │\n│ 25 try: │\n│ ❱ 26 1 / 0 │\n│ 27 except Exception: │\n│ 28 exc_type, exc_value, traceback = sys.exc_info() │\n│ 29 sys.excepthook(exc_type, exc_value, traceback) │\n╰──────────────────────────────────────────────────────────────────────────────────────────────────╯\nZeroDivisionError: division by zero\n' + + +def test_handler(): + console = Console(file=io.StringIO(), width=100, color_system=None) + expected_old_handler = sys.excepthook + try: + old_handler = install(console=console) + try: + 1 / 0 + except Exception: + exc_type, exc_value, traceback = sys.exc_info() + sys.excepthook(exc_type, exc_value, traceback) + rendered_exception = console.file.getvalue() + print(repr(rendered_exception)) + assert "Traceback" in rendered_exception + assert "ZeroDivisionError" in rendered_exception + finally: + sys.excepthook = old_handler + assert old_handler == expected_old_handler + + +def text_exception_render(): + exc_render = render(get_exception()) + assert exc_render == expected + + +def test_capture(): + try: + 1 / 0 + except Exception: + tb = Traceback() + assert tb.trace.stacks[0].exc_type == "ZeroDivisionError" + + +def test_no_exception(): + with pytest.raises(ValueError): + tb = Traceback() + + +def get_exception() -> Traceback: + def bar(a): + print(1 / a) + + def foo(a): + bar(a) + + try: + try: + foo(0) + except: + foobarbaz + except: + tb = Traceback() + return tb + + +def test_print_exception(): + console = Console(width=100, file=io.StringIO()) + try: + 1 / 0 + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + assert "ZeroDivisionError" in exception_text + + +def test_print_exception_locals(): + console = Console(width=100, file=io.StringIO()) + try: + 1 / 0 + except Exception: + console.print_exception(show_locals=True) + exception_text = console.file.getvalue() + print(exception_text) + assert "ZeroDivisionError" in exception_text + assert "locals" in exception_text + assert "console = <console width=100 None>" in exception_text + + +def test_syntax_error(): + console = Console(width=100, file=io.StringIO()) + try: + # raises SyntaxError: unexpected EOF while parsing + eval("(2 + 2") + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + assert "SyntaxError" in exception_text + + +def test_nested_exception(): + console = Console(width=100, file=io.StringIO()) + value_error_message = "ValueError because of ZeroDivisionError" + + try: + try: + 1 / 0 + except ZeroDivisionError: + raise ValueError(value_error_message) + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + + text_should_contain = [ + value_error_message, + "ZeroDivisionError", + "ValueError", + "During handling of the above exception", + ] + + for msg in text_should_contain: + assert msg in exception_text + + # ZeroDivisionError should come before ValueError + assert exception_text.find("ZeroDivisionError") < exception_text.find("ValueError") + + +def test_caused_exception(): + console = Console(width=100, file=io.StringIO()) + value_error_message = "ValueError caused by ZeroDivisionError" + + try: + try: + 1 / 0 + except ZeroDivisionError as e: + raise ValueError(value_error_message) from e + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + + text_should_contain = [ + value_error_message, + "ZeroDivisionError", + "ValueError", + "The above exception was the direct cause", + ] + + for msg in text_should_contain: + assert msg in exception_text + + # ZeroDivisionError should come before ValueError + assert exception_text.find("ZeroDivisionError") < exception_text.find("ValueError") + + +def test_filename_with_bracket(): + console = Console(width=100, file=io.StringIO()) + try: + exec(compile("1/0", filename="<string>", mode="exec")) + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + assert "<string>" in exception_text + + +def test_filename_not_a_file(): + console = Console(width=100, file=io.StringIO()) + try: + exec(compile("1/0", filename="string", mode="exec")) + except Exception: + console.print_exception() + exception_text = console.file.getvalue() + assert "string" in exception_text + + +def test_broken_str(): + class BrokenStr(Exception): + def __str__(self): + 1 / 0 + + console = Console(width=100, file=io.StringIO()) + try: + raise BrokenStr() + except Exception: + console.print_exception() + result = console.file.getvalue() + print(result) + assert "<exception str() failed>" in result + + +def test_guess_lexer(): + assert Traceback._guess_lexer("foo.py", "code") == "python" + code_python = "#! usr/bin/env python\nimport this" + assert Traceback._guess_lexer("foo", code_python) == "python" + assert Traceback._guess_lexer("foo", "foo\nbnar") == "text" + + +if __name__ == "__main__": # pragma: no cover + + expected = render(get_exception()) + + with open("_exception_render.py", "wt") as fh: + exc_render = render(get_exception()) + print(exc_render) + fh.write(f"expected={exc_render!r}") diff --git a/tests/test_tree.py b/tests/test_tree.py new file mode 100644 index 0000000..90dcd77 --- /dev/null +++ b/tests/test_tree.py @@ -0,0 +1,103 @@ +import sys + +import pytest + +from rich.console import Console +from rich.measure import Measurement +from rich.tree import Tree + + +def test_render_single_node(): + tree = Tree("foo") + console = Console(color_system=None, width=20) + console.begin_capture() + console.print(tree) + assert console.end_capture() == "foo \n" + + +def test_render_single_branch(): + tree = Tree("foo") + tree.add("bar") + console = Console(color_system=None, width=20) + console.begin_capture() + console.print(tree) + result = console.end_capture() + print(repr(result)) + expected = "foo \n└── bar \n" + assert result == expected + + +def test_render_double_branch(): + tree = Tree("foo") + tree.add("bar") + tree.add("baz") + console = Console(color_system=None, width=20) + console.begin_capture() + console.print(tree) + result = console.end_capture() + print(repr(result)) + expected = "foo \n├── bar \n└── baz \n" + assert result == expected + + +def test_render_ascii(): + tree = Tree("foo") + tree.add("bar") + tree.add("baz") + + class AsciiConsole(Console): + @property + def encoding(self): + return "ascii" + + console = AsciiConsole(color_system=None, width=20) + console.begin_capture() + console.print(tree) + result = console.end_capture() + expected = "foo \n+-- bar \n`-- baz \n" + assert result == expected + + +@pytest.mark.skipif(sys.platform == "win32", reason="different on Windows") +def test_render(): + tree = Tree("foo") + tree.add("bar", style="italic") + baz_tree = tree.add("baz", guide_style="bold red", style="on blue") + baz_tree.add("1") + baz_tree.add("2") + tree.add("egg") + + console = Console(width=20, force_terminal=True, color_system="standard") + console.begin_capture() + console.print(tree) + result = console.end_capture() + print(repr(result)) + expected = "foo \n├── \x1b[3mbar\x1b[0m\x1b[3m \x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\x1b[44m \x1b[0m\n└── egg \n" + assert result == expected + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows specific") +def test_render(): + tree = Tree("foo") + tree.add("bar", style="italic") + baz_tree = tree.add("baz", guide_style="bold red", style="on blue") + baz_tree.add("1") + baz_tree.add("2") + tree.add("egg") + + console = Console(width=20, force_terminal=True, color_system="standard") + console.begin_capture() + console.print(tree) + result = console.end_capture() + print(repr(result)) + expected = "foo \n├── \x1b[3mbar\x1b[0m\x1b[3m \x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[31;44m├── \x1b[0m\x1b[44m1\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[31;44m└── \x1b[0m\x1b[44m2\x1b[0m\x1b[44m \x1b[0m\n└── egg \n" + assert result == expected + + +def test_tree_measure(): + tree = Tree("foo") + tree.add("bar") + tree.add("musroom risotto") + console = Console() + measurement = Measurement.get(console, tree) + assert measurement == Measurement(11, 19) |