prettytable.py 109 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2009-2014, Luke Maurits <luke@maurits.id.au>
  4. # All rights reserved.
  5. # With contributions from:
  6. # * Chris Clark
  7. # * Klein Stephane
  8. # * John Filleau
  9. # * Vladimir Vrzić
  10. #
  11. # Redistribution and use in source and binary forms, with or without
  12. # modification, are permitted provided that the following conditions are met:
  13. #
  14. # * Redistributions of source code must retain the above copyright notice,
  15. # this list of conditions and the following disclaimer.
  16. # * Redistributions in binary form must reproduce the above copyright notice,
  17. # this list of conditions and the following disclaimer in the documentation
  18. # and/or other materials provided with the distribution.
  19. # * The name of the author may not be used to endorse or promote products
  20. # derived from this software without specific prior written permission.
  21. #
  22. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  23. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  24. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  25. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  26. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  27. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  28. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  29. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  30. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  31. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  32. # POSSIBILITY OF SUCH DAMAGE.
  33. from __future__ import annotations
  34. import io
  35. import re
  36. from enum import IntEnum
  37. from functools import lru_cache
  38. from html.parser import HTMLParser
  39. from typing import Any, Literal, TypedDict, cast
  40. TYPE_CHECKING = False
  41. if TYPE_CHECKING:
  42. from collections.abc import Callable, Mapping, Sequence
  43. from sqlite3 import Cursor
  44. from typing import Final, TypeAlias
  45. from _typeshed import SupportsRichComparison
  46. from typing_extensions import Self
  47. class HRuleStyle(IntEnum):
  48. FRAME = 0
  49. ALL = 1
  50. NONE = 2
  51. HEADER = 3
  52. class VRuleStyle(IntEnum):
  53. FRAME = 0
  54. ALL = 1
  55. NONE = 2
  56. class TableStyle(IntEnum):
  57. DEFAULT = 10
  58. MSWORD_FRIENDLY = 11
  59. PLAIN_COLUMNS = 12
  60. MARKDOWN = 13
  61. ORGMODE = 14
  62. DOUBLE_BORDER = 15
  63. SINGLE_BORDER = 16
  64. RANDOM = 20
  65. # keep for backwards compatibility
  66. _DEPRECATED_FRAME: Final = 0
  67. _DEPRECATED_ALL: Final = 1
  68. _DEPRECATED_NONE: Final = 2
  69. _DEPRECATED_HEADER: Final = 3
  70. _DEPRECATED_DEFAULT: Final = TableStyle.DEFAULT
  71. _DEPRECATED_MSWORD_FRIENDLY: Final = TableStyle.MSWORD_FRIENDLY
  72. _DEPRECATED_PLAIN_COLUMNS: Final = TableStyle.PLAIN_COLUMNS
  73. _DEPRECATED_MARKDOWN: Final = TableStyle.MARKDOWN
  74. _DEPRECATED_ORGMODE: Final = TableStyle.ORGMODE
  75. _DEPRECATED_DOUBLE_BORDER: Final = TableStyle.DOUBLE_BORDER
  76. _DEPRECATED_SINGLE_BORDER: Final = TableStyle.SINGLE_BORDER
  77. _DEPRECATED_RANDOM: Final = TableStyle.RANDOM
  78. # --------------------------------
  79. BASE_ALIGN_VALUE: Final = "base_align_value"
  80. RowType: TypeAlias = list[Any]
  81. AlignType: TypeAlias = Literal["l", "c", "r"]
  82. VAlignType: TypeAlias = Literal["t", "m", "b"]
  83. HeaderStyleType: TypeAlias = Literal["cap", "title", "upper", "lower"] | None
  84. class OptionsType(TypedDict):
  85. title: str | None
  86. start: int
  87. end: int | None
  88. fields: Sequence[str | None] | None
  89. header: bool
  90. use_header_width: bool
  91. border: bool
  92. preserve_internal_border: bool
  93. sortby: str | None
  94. reversesort: bool
  95. sort_key: Callable[[RowType], SupportsRichComparison]
  96. row_filter: Callable[[RowType], bool]
  97. attributes: dict[str, str]
  98. format: bool
  99. hrules: HRuleStyle
  100. vrules: VRuleStyle
  101. int_format: str | dict[str, str] | None
  102. float_format: str | dict[str, str] | None
  103. custom_format: (
  104. Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
  105. )
  106. min_table_width: int | None
  107. max_table_width: int | None
  108. padding_width: int
  109. left_padding_width: int | None
  110. right_padding_width: int | None
  111. vertical_char: str
  112. horizontal_char: str
  113. horizontal_align_char: str
  114. junction_char: str
  115. header_style: HeaderStyleType
  116. xhtml: bool
  117. print_empty: bool
  118. oldsortslice: bool
  119. top_junction_char: str
  120. bottom_junction_char: str
  121. right_junction_char: str
  122. left_junction_char: str
  123. top_right_junction_char: str
  124. top_left_junction_char: str
  125. bottom_right_junction_char: str
  126. bottom_left_junction_char: str
  127. align: dict[str, AlignType]
  128. valign: dict[str, VAlignType]
  129. min_width: int | dict[str, int] | None
  130. max_width: int | dict[str, int] | None
  131. none_format: str | dict[str, str | None] | None
  132. escape_header: bool
  133. escape_data: bool
  134. break_on_hyphens: bool
  135. # ANSI colour codes
  136. _re = re.compile(r"\033\[[0-9;]*m|\033\(B")
  137. # OSC 8 hyperlinks
  138. _osc8_re = re.compile(r"\033\]8;;.*?\033\\(.*?)\033\]8;;\033\\")
  139. @lru_cache
  140. def _get_size(text: str) -> tuple[int, int]:
  141. lines = text.split("\n")
  142. height = len(lines)
  143. width = max(_str_block_width(line) for line in lines)
  144. return width, height
  145. class PrettyTable:
  146. _xhtml: bool
  147. _align: dict[str, AlignType]
  148. _valign: dict[str, VAlignType]
  149. _min_width: dict[str, int]
  150. _max_width: dict[str, int]
  151. _min_table_width: int | None
  152. _max_table_width: int | None
  153. _fields: Sequence[str | None] | None
  154. _title: str | None
  155. _start: int
  156. _end: int | None
  157. _sortby: str | None
  158. _reversesort: bool
  159. _sort_key: Callable[[RowType], SupportsRichComparison]
  160. _row_filter: Callable[[RowType], bool]
  161. _header: bool
  162. _use_header_width: bool
  163. _header_style: HeaderStyleType
  164. _border: bool
  165. _preserve_internal_border: bool
  166. _hrules: HRuleStyle
  167. _vrules: VRuleStyle
  168. _int_format: dict[str, str]
  169. _float_format: dict[str, str]
  170. _custom_format: dict[str, Callable[[str, Any], str]]
  171. _padding_width: int
  172. _left_padding_width: int | None
  173. _right_padding_width: int | None
  174. _vertical_char: str
  175. _horizontal_char: str
  176. _horizontal_align_char: str | None
  177. _junction_char: str
  178. _top_junction_char: str | None
  179. _bottom_junction_char: str | None
  180. _right_junction_char: str | None
  181. _left_junction_char: str | None
  182. _top_right_junction_char: str | None
  183. _top_left_junction_char: str | None
  184. _bottom_right_junction_char: str | None
  185. _bottom_left_junction_char: str | None
  186. _format: bool
  187. _print_empty: bool
  188. _oldsortslice: bool
  189. _attributes: dict[str, str]
  190. _escape_header: bool
  191. _escape_data: bool
  192. _style: TableStyle | None
  193. orgmode: bool
  194. _widths: list[int]
  195. _hrule: str
  196. _break_on_hyphens: bool
  197. def __init__(self, field_names: Sequence[str] | None = None, **kwargs) -> None:
  198. """Return a new PrettyTable instance
  199. Arguments:
  200. encoding - Unicode encoding scheme used to decode any encoded input
  201. title - optional table title
  202. field_names - list or tuple of field names
  203. fields - list or tuple of field names to include in displays
  204. start - index of first data row to include in output
  205. end - index of last data row to include in output PLUS ONE (list slice style)
  206. header - print a header showing field names (True or False)
  207. use_header_width - reflect width of header (True or False)
  208. header_style - stylisation to apply to field names in header
  209. ("cap", "title", "upper", "lower" or None)
  210. border - print a border around the table (True or False)
  211. preserve_internal_border - print a border inside the table even if
  212. border is disabled (True or False)
  213. hrules - controls printing of horizontal rules after rows.
  214. Allowed values: HRuleStyle
  215. vrules - controls printing of vertical rules between columns.
  216. Allowed values: VRuleStyle
  217. int_format - controls formatting of integer data
  218. float_format - controls formatting of floating point data
  219. custom_format - controls formatting of any column using callable
  220. min_table_width - minimum desired table width, in characters
  221. max_table_width - maximum desired table width, in characters
  222. min_width - minimum desired field width, in characters
  223. max_width - maximum desired field width, in characters
  224. padding_width - number of spaces on either side of column data
  225. (only used if left and right paddings are None)
  226. left_padding_width - number of spaces on left hand side of column data
  227. right_padding_width - number of spaces on right hand side of column data
  228. vertical_char - single character string used to draw vertical lines
  229. horizontal_char - single character string used to draw horizontal lines
  230. horizontal_align_char - single character string used to indicate alignment
  231. junction_char - single character string used to draw line junctions
  232. top_junction_char - single character string used to draw top line junctions
  233. bottom_junction_char -
  234. single character string used to draw bottom line junctions
  235. right_junction_char - single character string used to draw right line junctions
  236. left_junction_char - single character string used to draw left line junctions
  237. top_right_junction_char -
  238. single character string used to draw top-right line junctions
  239. top_left_junction_char -
  240. single character string used to draw top-left line junctions
  241. bottom_right_junction_char -
  242. single character string used to draw bottom-right line junctions
  243. bottom_left_junction_char -
  244. single character string used to draw bottom-left line junctions
  245. sortby - name of field to sort rows by
  246. sort_key - sorting key function, applied to data points before sorting
  247. row_filter - filter function applied on rows
  248. align - default align for each column (None, "l", "c" or "r")
  249. valign - default valign for each row (None, "t", "m" or "b")
  250. reversesort - True or False to sort in descending or ascending order
  251. oldsortslice - Slice rows before sorting in the "old style"
  252. break_on_hyphens - Whether long lines are broken on hypens or not, default: True
  253. """
  254. self.encoding = kwargs.get("encoding", "UTF-8")
  255. # Data
  256. self._field_names: list[str] = []
  257. self._rows: list[RowType] = []
  258. self._dividers: list[bool] = []
  259. self.align = {}
  260. self.valign = {}
  261. self.max_width = {}
  262. self.min_width = {}
  263. self.int_format = {}
  264. self.float_format = {}
  265. self.custom_format = {}
  266. self._style = None
  267. # Options
  268. self._options = [
  269. "title",
  270. "start",
  271. "end",
  272. "fields",
  273. "header",
  274. "use_header_width",
  275. "border",
  276. "preserve_internal_border",
  277. "sortby",
  278. "reversesort",
  279. "sort_key",
  280. "row_filter",
  281. "attributes",
  282. "format",
  283. "hrules",
  284. "vrules",
  285. "int_format",
  286. "float_format",
  287. "custom_format",
  288. "min_table_width",
  289. "max_table_width",
  290. "padding_width",
  291. "left_padding_width",
  292. "right_padding_width",
  293. "vertical_char",
  294. "horizontal_char",
  295. "horizontal_align_char",
  296. "junction_char",
  297. "header_style",
  298. "xhtml",
  299. "print_empty",
  300. "oldsortslice",
  301. "top_junction_char",
  302. "bottom_junction_char",
  303. "right_junction_char",
  304. "left_junction_char",
  305. "top_right_junction_char",
  306. "top_left_junction_char",
  307. "bottom_right_junction_char",
  308. "bottom_left_junction_char",
  309. "align",
  310. "valign",
  311. "max_width",
  312. "min_width",
  313. "none_format",
  314. "escape_header",
  315. "escape_data",
  316. "break_on_hyphens",
  317. ]
  318. self._none_format: dict[str, str | None] = {}
  319. self._kwargs = {}
  320. if field_names:
  321. self.field_names = field_names
  322. else:
  323. self._widths: list[int] = []
  324. for option in self._options:
  325. if option in kwargs:
  326. self._validate_option(option, kwargs[option])
  327. self._kwargs[option] = kwargs[option]
  328. else:
  329. kwargs[option] = None
  330. self._kwargs[option] = None
  331. self._title = kwargs["title"] or None
  332. self._start = kwargs["start"] or 0
  333. self._end = kwargs["end"] or None
  334. self._fields = kwargs["fields"] or None
  335. if kwargs["header"] in (True, False):
  336. self._header = kwargs["header"]
  337. else:
  338. self._header = True
  339. if kwargs["use_header_width"] in (True, False):
  340. self._use_header_width = kwargs["use_header_width"]
  341. else:
  342. self._use_header_width = True
  343. self._header_style = kwargs["header_style"] or None
  344. if kwargs["border"] in (True, False):
  345. self._border = kwargs["border"]
  346. else:
  347. self._border = True
  348. if kwargs["preserve_internal_border"] in (True, False):
  349. self._preserve_internal_border = kwargs["preserve_internal_border"]
  350. else:
  351. self._preserve_internal_border = False
  352. self._hrules = kwargs["hrules"] or HRuleStyle.FRAME
  353. self._vrules = kwargs["vrules"] or VRuleStyle.ALL
  354. self._sortby = kwargs["sortby"] or None
  355. if kwargs["reversesort"] in (True, False):
  356. self._reversesort = kwargs["reversesort"]
  357. else:
  358. self._reversesort = False
  359. self._sort_key = kwargs["sort_key"] or (lambda x: x)
  360. self._row_filter = kwargs["row_filter"] or (lambda x: True)
  361. if kwargs["escape_data"] in (True, False):
  362. self._escape_data = kwargs["escape_data"]
  363. else:
  364. self._escape_data = True
  365. if kwargs["escape_header"] in (True, False):
  366. self._escape_header = kwargs["escape_header"]
  367. else:
  368. self._escape_header = True
  369. self._column_specific_args()
  370. self._min_table_width = kwargs["min_table_width"] or None
  371. self._max_table_width = kwargs["max_table_width"] or None
  372. if kwargs["padding_width"] is None:
  373. self._padding_width = 1
  374. else:
  375. self._padding_width = kwargs["padding_width"]
  376. self._left_padding_width = kwargs["left_padding_width"] or None
  377. self._right_padding_width = kwargs["right_padding_width"] or None
  378. self._vertical_char = kwargs["vertical_char"] or "|"
  379. self._horizontal_char = kwargs["horizontal_char"] or "-"
  380. self._horizontal_align_char = kwargs["horizontal_align_char"]
  381. self._junction_char = kwargs["junction_char"] or "+"
  382. self._top_junction_char = kwargs["top_junction_char"]
  383. self._bottom_junction_char = kwargs["bottom_junction_char"]
  384. self._right_junction_char = kwargs["right_junction_char"]
  385. self._left_junction_char = kwargs["left_junction_char"]
  386. self._top_right_junction_char = kwargs["top_right_junction_char"]
  387. self._top_left_junction_char = kwargs["top_left_junction_char"]
  388. self._bottom_right_junction_char = kwargs["bottom_right_junction_char"]
  389. self._bottom_left_junction_char = kwargs["bottom_left_junction_char"]
  390. if kwargs["print_empty"] in (True, False):
  391. self._print_empty = kwargs["print_empty"]
  392. else:
  393. self._print_empty = True
  394. if kwargs["oldsortslice"] in (True, False):
  395. self._oldsortslice = kwargs["oldsortslice"]
  396. else:
  397. self._oldsortslice = False
  398. self._format = kwargs["format"] or False
  399. self._xhtml = kwargs["xhtml"] or False
  400. self._attributes = kwargs["attributes"] or {}
  401. if kwargs["break_on_hyphens"] in (True, False):
  402. self._break_on_hyphens = kwargs["break_on_hyphens"]
  403. else:
  404. self._break_on_hyphens = True
  405. def _column_specific_args(self) -> None:
  406. # Column specific arguments, use property.setters
  407. for attr in (
  408. "align",
  409. "valign",
  410. "max_width",
  411. "min_width",
  412. "int_format",
  413. "float_format",
  414. "custom_format",
  415. "none_format",
  416. ):
  417. setattr(
  418. self, attr, (self._kwargs[attr] or {}) if attr in self._kwargs else {}
  419. )
  420. def _justify(self, text: str, width: int, align: AlignType) -> str:
  421. excess = width - _str_block_width(text)
  422. if align == "l":
  423. return text + excess * " "
  424. elif align == "r":
  425. return excess * " " + text
  426. else:
  427. if excess % 2:
  428. # Uneven padding
  429. # Put more space on right if text is of odd length...
  430. if _str_block_width(text) % 2:
  431. return (excess // 2) * " " + text + (excess // 2 + 1) * " "
  432. # and more space on left if text is of even length
  433. else:
  434. return (excess // 2 + 1) * " " + text + (excess // 2) * " "
  435. # Why distribute extra space this way? To match the behaviour of
  436. # the inbuilt str.center() method.
  437. else:
  438. # Equal padding on either side
  439. return (excess // 2) * " " + text + (excess // 2) * " "
  440. def __getattr__(self, name):
  441. if name == "rowcount":
  442. return len(self._rows)
  443. elif name == "colcount":
  444. if self._field_names:
  445. return len(self._field_names)
  446. elif self._rows:
  447. return len(self._rows[0])
  448. else:
  449. return 0
  450. else:
  451. raise AttributeError(name)
  452. def __getitem__(self, index: int | slice) -> PrettyTable:
  453. new = PrettyTable()
  454. new.field_names = self.field_names
  455. for attr in self._options:
  456. setattr(new, "_" + attr, getattr(self, "_" + attr))
  457. setattr(new, "_align", getattr(self, "_align"))
  458. if isinstance(index, slice):
  459. for row in self._rows[index]:
  460. new.add_row(row)
  461. elif isinstance(index, int):
  462. new.add_row(self._rows[index])
  463. else:
  464. msg = f"Index {index} is invalid, must be an integer or slice"
  465. raise IndexError(msg)
  466. return new
  467. def __str__(self) -> str:
  468. return self.get_string()
  469. def __repr__(self) -> str:
  470. return self.get_string()
  471. def _repr_html_(self) -> str:
  472. """
  473. Returns get_html_string value by default
  474. as the repr call in Jupyter notebook environment
  475. """
  476. return self.get_html_string()
  477. ##############################
  478. # ATTRIBUTE VALIDATORS #
  479. ##############################
  480. # The method _validate_option is all that should be used elsewhere in the code base
  481. # to validate options. It will call the appropriate validation method for that
  482. # option. The individual validation methods should never need to be called directly
  483. # (although nothing bad will happen if they *are*).
  484. # Validation happens in TWO places.
  485. # Firstly, in the property setters defined in the ATTRIBUTE MANAGEMENT section.
  486. # Secondly, in the _get_options method, where keyword arguments are mixed with
  487. # persistent settings
  488. def _validate_option(self, option, val) -> None:
  489. if option == "field_names":
  490. self._validate_field_names(val)
  491. elif option == "none_format":
  492. self._validate_none_format(val)
  493. elif option in (
  494. "start",
  495. "end",
  496. "max_width",
  497. "min_width",
  498. "min_table_width",
  499. "max_table_width",
  500. "padding_width",
  501. "left_padding_width",
  502. "right_padding_width",
  503. ):
  504. self._validate_nonnegative_int(option, val)
  505. elif option == "sortby":
  506. self._validate_field_name(option, val)
  507. elif option in ("sort_key", "row_filter"):
  508. self._validate_function(option, val)
  509. elif option == "hrules":
  510. self._validate_hrules(option, val)
  511. elif option == "vrules":
  512. self._validate_vrules(option, val)
  513. elif option == "fields":
  514. self._validate_all_field_names(option, val)
  515. elif option in (
  516. "header",
  517. "use_header_width",
  518. "border",
  519. "preserve_internal_border",
  520. "reversesort",
  521. "xhtml",
  522. "format",
  523. "print_empty",
  524. "oldsortslice",
  525. "escape_header",
  526. "escape_data",
  527. "break_on_hyphens",
  528. ):
  529. self._validate_true_or_false(option, val)
  530. elif option == "header_style":
  531. self._validate_header_style(val)
  532. elif option == "int_format":
  533. self._validate_int_format(option, val)
  534. elif option == "float_format":
  535. self._validate_float_format(option, val)
  536. elif option == "custom_format":
  537. for k, formatter in val.items():
  538. self._validate_function(f"{option}.{k}", formatter)
  539. elif option in (
  540. "vertical_char",
  541. "horizontal_char",
  542. "horizontal_align_char",
  543. "junction_char",
  544. "top_junction_char",
  545. "bottom_junction_char",
  546. "right_junction_char",
  547. "left_junction_char",
  548. "top_right_junction_char",
  549. "top_left_junction_char",
  550. "bottom_right_junction_char",
  551. "bottom_left_junction_char",
  552. ):
  553. self._validate_single_char(option, val)
  554. elif option == "attributes":
  555. self._validate_attributes(option, val)
  556. def _validate_field_names(self, val):
  557. # Check for appropriate length
  558. if self._field_names:
  559. try:
  560. assert len(val) == len(self._field_names)
  561. except AssertionError:
  562. msg = (
  563. "Field name list has incorrect number of values, "
  564. f"(actual) {len(val)}!={len(self._field_names)} (expected)"
  565. )
  566. raise ValueError(msg)
  567. if self._rows:
  568. try:
  569. assert len(val) == len(self._rows[0])
  570. except AssertionError:
  571. msg = (
  572. "Field name list has incorrect number of values, "
  573. f"(actual) {len(val)}!={len(self._rows[0])} (expected)"
  574. )
  575. raise ValueError(msg)
  576. # Check for uniqueness
  577. try:
  578. assert len(val) == len(set(val))
  579. except AssertionError:
  580. msg = "Field names must be unique"
  581. raise ValueError(msg)
  582. def _validate_none_format(self, val):
  583. try:
  584. if val is not None:
  585. assert isinstance(val, str)
  586. except AssertionError:
  587. msg = "Replacement for None value must be a string if being supplied."
  588. raise TypeError(msg)
  589. def _validate_header_style(self, val):
  590. try:
  591. assert val in ("cap", "title", "upper", "lower", None)
  592. except AssertionError:
  593. msg = "Invalid header style, use cap, title, upper, lower or None"
  594. raise ValueError(msg)
  595. def _validate_align(self, val):
  596. try:
  597. assert val in ["l", "c", "r"]
  598. except AssertionError:
  599. msg = f"Alignment {val} is invalid, use l, c or r"
  600. raise ValueError(msg)
  601. def _validate_valign(self, val):
  602. try:
  603. assert val in ["t", "m", "b"]
  604. except AssertionError:
  605. msg = f"Alignment {val} is invalid, use t, m, b"
  606. raise ValueError(msg)
  607. def _validate_nonnegative_int(self, name, val):
  608. try:
  609. assert int(val) >= 0
  610. except AssertionError:
  611. msg = f"Invalid value for {name}: {val}"
  612. raise ValueError(msg)
  613. def _validate_true_or_false(self, name, val):
  614. try:
  615. assert val in (True, False)
  616. except AssertionError:
  617. msg = f"Invalid value for {name}. Must be True or False."
  618. raise ValueError(msg)
  619. def _validate_int_format(self, name, val):
  620. if val == "":
  621. return
  622. try:
  623. assert isinstance(val, str)
  624. assert val.isdigit()
  625. except AssertionError:
  626. msg = f"Invalid value for {name}. Must be an integer format string."
  627. raise ValueError(msg)
  628. def _validate_float_format(self, name, val):
  629. if val == "":
  630. return
  631. try:
  632. assert isinstance(val, str)
  633. assert "." in val
  634. bits = val.split(".")
  635. assert len(bits) <= 2
  636. assert bits[0] == "" or bits[0].isdigit()
  637. assert (
  638. bits[1] == ""
  639. or bits[1].isdigit()
  640. or (bits[1][-1] == "f" and bits[1].rstrip("f").isdigit())
  641. )
  642. except AssertionError:
  643. msg = f"Invalid value for {name}. Must be a float format string."
  644. raise ValueError(msg)
  645. def _validate_function(self, name, val):
  646. try:
  647. assert hasattr(val, "__call__")
  648. except AssertionError:
  649. msg = f"Invalid value for {name}. Must be a function."
  650. raise ValueError(msg)
  651. def _validate_hrules(self, name, val):
  652. try:
  653. assert val in list(HRuleStyle)
  654. except AssertionError:
  655. msg = f"Invalid value for {name}. Must be HRuleStyle."
  656. raise ValueError(msg)
  657. def _validate_vrules(self, name, val):
  658. try:
  659. assert val in list(VRuleStyle)
  660. except AssertionError:
  661. msg = f"Invalid value for {name}. Must be VRuleStyle."
  662. raise ValueError(msg)
  663. def _validate_field_name(self, name, val):
  664. try:
  665. assert (val in self._field_names) or (val is None)
  666. except AssertionError:
  667. msg = f"Invalid field name: {val}"
  668. raise ValueError(msg)
  669. def _validate_all_field_names(self, name, val):
  670. try:
  671. for x in val:
  672. self._validate_field_name(name, x)
  673. except AssertionError:
  674. msg = "Fields must be a sequence of field names"
  675. raise ValueError(msg)
  676. def _validate_single_char(self, name, val):
  677. try:
  678. assert _str_block_width(val) == 1
  679. except AssertionError:
  680. msg = f"Invalid value for {name}. Must be a string of length 1."
  681. raise ValueError(msg)
  682. def _validate_attributes(self, name, val):
  683. try:
  684. assert isinstance(val, dict)
  685. except AssertionError:
  686. msg = "Attributes must be a dictionary of name/value pairs"
  687. raise TypeError(msg)
  688. ##############################
  689. # ATTRIBUTE MANAGEMENT #
  690. ##############################
  691. @property
  692. def rows(self) -> list[RowType]:
  693. return self._rows[:]
  694. @property
  695. def dividers(self) -> list[bool]:
  696. return self._dividers[:]
  697. @property
  698. def xhtml(self) -> bool:
  699. """Print <br/> tags if True, <br> tags if False"""
  700. return self._xhtml
  701. @xhtml.setter
  702. def xhtml(self, val: bool) -> None:
  703. self._validate_option("xhtml", val)
  704. self._xhtml = val
  705. @property
  706. def none_format(self) -> dict[str, str | None]:
  707. return self._none_format
  708. @none_format.setter
  709. def none_format(self, val: str | dict[str, str | None] | None):
  710. """Representation of None values:
  711. Arguments:
  712. val - The alternative representation to be used for None values
  713. """
  714. if not self._field_names:
  715. self._none_format = {}
  716. elif isinstance(val, str):
  717. for field in self._field_names:
  718. self._none_format[field] = None
  719. self._validate_none_format(val)
  720. for field in self._field_names:
  721. self._none_format[field] = val
  722. elif isinstance(val, dict) and val:
  723. for field, fval in val.items():
  724. self._validate_none_format(fval)
  725. self._none_format[field] = fval
  726. else:
  727. for field in self._field_names:
  728. self._none_format[field] = None
  729. @property
  730. def field_names(self) -> list[str]:
  731. """List or tuple of field names
  732. When setting field_names, if there are already field names the new list
  733. of field names must be the same length. Columns are renamed and row data
  734. remains unchanged."""
  735. return self._field_names
  736. @field_names.setter
  737. def field_names(self, val: Sequence[Any]) -> None:
  738. val = cast("list[str]", [str(x) for x in val])
  739. self._validate_option("field_names", val)
  740. old_names = None
  741. if self._field_names:
  742. old_names = self._field_names[:]
  743. self._field_names = val
  744. self._column_specific_args()
  745. if self._align and old_names:
  746. for old_name, new_name in zip(old_names, val):
  747. self._align[new_name] = self._align[old_name]
  748. for old_name in old_names:
  749. if old_name not in self._align:
  750. self._align.pop(old_name)
  751. elif self._align:
  752. for field_name in self._field_names:
  753. self._align[field_name] = self._align[BASE_ALIGN_VALUE]
  754. else:
  755. self.align = "c"
  756. if self._valign and old_names:
  757. for old_name, new_name in zip(old_names, val):
  758. self._valign[new_name] = self._valign[old_name]
  759. for old_name in old_names:
  760. if old_name not in self._valign:
  761. self._valign.pop(old_name)
  762. else:
  763. self.valign = "t"
  764. @property
  765. def align(self) -> dict[str, AlignType]:
  766. """Controls alignment of fields
  767. Arguments:
  768. align - alignment, one of "l", "c", or "r" """
  769. return self._align
  770. @align.setter
  771. def align(self, val: AlignType | dict[str, AlignType] | None) -> None:
  772. if isinstance(val, str):
  773. self._validate_align(val)
  774. if not self._field_names:
  775. self._align = {BASE_ALIGN_VALUE: val}
  776. else:
  777. for field in self._field_names:
  778. self._align[field] = val
  779. elif isinstance(val, dict) and val:
  780. for field, fval in val.items():
  781. self._validate_align(fval)
  782. self._align[field] = fval
  783. else:
  784. if not self._field_names:
  785. self._align = {BASE_ALIGN_VALUE: "c"}
  786. else:
  787. for field in self._field_names:
  788. self._align[field] = "c"
  789. @property
  790. def valign(self) -> dict[str, VAlignType]:
  791. """Controls vertical alignment of fields
  792. Arguments:
  793. valign - vertical alignment, one of "t", "m", or "b" """
  794. return self._valign
  795. @valign.setter
  796. def valign(self, val: VAlignType | dict[str, VAlignType] | None) -> None:
  797. if not self._field_names:
  798. self._valign = {}
  799. if isinstance(val, str):
  800. self._validate_valign(val)
  801. for field in self._field_names:
  802. self._valign[field] = val
  803. elif isinstance(val, dict) and val:
  804. for field, fval in val.items():
  805. self._validate_valign(fval)
  806. self._valign[field] = fval
  807. else:
  808. for field in self._field_names:
  809. self._valign[field] = "t"
  810. @property
  811. def max_width(self) -> dict[str, int]:
  812. """Controls maximum width of fields
  813. Arguments:
  814. max_width - maximum width integer"""
  815. return self._max_width
  816. @max_width.setter
  817. def max_width(self, val: int | dict[str, int] | None) -> None:
  818. if isinstance(val, int):
  819. self._validate_option("max_width", val)
  820. for field in self._field_names:
  821. self._max_width[field] = val
  822. elif isinstance(val, dict) and val:
  823. for field, fval in val.items():
  824. self._validate_option("max_width", fval)
  825. self._max_width[field] = fval
  826. else:
  827. self._max_width = {}
  828. @property
  829. def min_width(self) -> dict[str, int]:
  830. """Controls minimum width of fields
  831. Arguments:
  832. min_width - minimum width integer"""
  833. return self._min_width
  834. @min_width.setter
  835. def min_width(self, val: int | dict[str, int] | None) -> None:
  836. if isinstance(val, int):
  837. self._validate_option("min_width", val)
  838. for field in self._field_names:
  839. self._min_width[field] = val
  840. elif isinstance(val, dict) and val:
  841. for field, fval in val.items():
  842. self._validate_option("min_width", fval)
  843. self._min_width[field] = fval
  844. else:
  845. self._min_width = {}
  846. @property
  847. def min_table_width(self) -> int | None:
  848. return self._min_table_width
  849. @min_table_width.setter
  850. def min_table_width(self, val: int) -> None:
  851. self._validate_option("min_table_width", val)
  852. self._min_table_width = val
  853. @property
  854. def max_table_width(self) -> int | None:
  855. return self._max_table_width
  856. @max_table_width.setter
  857. def max_table_width(self, val: int) -> None:
  858. self._validate_option("max_table_width", val)
  859. self._max_table_width = val
  860. @property
  861. def fields(self) -> Sequence[str | None] | None:
  862. """List or tuple of field names to include in displays"""
  863. return self._fields
  864. @fields.setter
  865. def fields(self, val: Sequence[str | None]) -> None:
  866. self._validate_option("fields", val)
  867. self._fields = val
  868. @property
  869. def title(self) -> str | None:
  870. """Optional table title
  871. Arguments:
  872. title - table title"""
  873. return self._title
  874. @title.setter
  875. def title(self, val: str) -> None:
  876. self._title = str(val)
  877. @property
  878. def start(self) -> int:
  879. """Start index of the range of rows to print
  880. Arguments:
  881. start - index of first data row to include in output"""
  882. return self._start
  883. @start.setter
  884. def start(self, val: int) -> None:
  885. self._validate_option("start", val)
  886. self._start = val
  887. @property
  888. def end(self) -> int | None:
  889. """End index of the range of rows to print
  890. Arguments:
  891. end - index of last data row to include in output PLUS ONE (list slice style)"""
  892. return self._end
  893. @end.setter
  894. def end(self, val: int) -> None:
  895. self._validate_option("end", val)
  896. self._end = val
  897. @property
  898. def sortby(self) -> str | None:
  899. """Name of field by which to sort rows
  900. Arguments:
  901. sortby - field name to sort by"""
  902. return self._sortby
  903. @sortby.setter
  904. def sortby(self, val: str | None) -> None:
  905. self._validate_option("sortby", val)
  906. self._sortby = val
  907. @property
  908. def reversesort(self) -> bool:
  909. """Controls direction of sorting (ascending vs descending)
  910. Arguments:
  911. reveresort - set to True to sort by descending order, or False to sort by
  912. ascending order"""
  913. return self._reversesort
  914. @reversesort.setter
  915. def reversesort(self, val: bool) -> None:
  916. self._validate_option("reversesort", val)
  917. self._reversesort = val
  918. @property
  919. def sort_key(self) -> Callable[[RowType], SupportsRichComparison]:
  920. """Sorting key function, applied to data points before sorting
  921. Arguments:
  922. sort_key - a function which takes one argument and returns something to be
  923. sorted"""
  924. return self._sort_key
  925. @sort_key.setter
  926. def sort_key(self, val: Callable[[RowType], SupportsRichComparison]) -> None:
  927. self._validate_option("sort_key", val)
  928. self._sort_key = val
  929. @property
  930. def row_filter(self) -> Callable[[RowType], bool]:
  931. """Filter function, applied to data points
  932. Arguments:
  933. row_filter - a function which takes one argument and returns a Boolean"""
  934. return self._row_filter
  935. @row_filter.setter
  936. def row_filter(self, val: Callable[[RowType], bool]) -> None:
  937. self._validate_option("row_filter", val)
  938. self._row_filter = val
  939. @property
  940. def header(self) -> bool:
  941. """Controls printing of table header with field names
  942. Arguments:
  943. header - print a header showing field names (True or False)"""
  944. return self._header
  945. @header.setter
  946. def header(self, val: bool) -> None:
  947. self._validate_option("header", val)
  948. self._header = val
  949. @property
  950. def use_header_width(self) -> bool:
  951. """Controls whether header is included in computing width
  952. Arguments:
  953. use_header_width - respect width of fieldname in header to calculate column
  954. width (True or False)
  955. """
  956. return self._use_header_width
  957. @use_header_width.setter
  958. def use_header_width(self, val: bool) -> None:
  959. self._validate_option("use_header_width", val)
  960. self._use_header_width = val
  961. @property
  962. def header_style(self) -> HeaderStyleType:
  963. """Controls stylisation applied to field names in header
  964. Arguments:
  965. header_style - stylisation to apply to field names in header
  966. ("cap", "title", "upper", "lower" or None)"""
  967. return self._header_style
  968. @header_style.setter
  969. def header_style(self, val: HeaderStyleType) -> None:
  970. self._validate_header_style(val)
  971. self._header_style = val
  972. @property
  973. def border(self) -> bool:
  974. """Controls printing of border around table
  975. Arguments:
  976. border - print a border around the table (True or False)"""
  977. return self._border
  978. @border.setter
  979. def border(self, val: bool) -> None:
  980. self._validate_option("border", val)
  981. self._border = val
  982. @property
  983. def preserve_internal_border(self) -> bool:
  984. """Controls printing of border inside table
  985. Arguments:
  986. preserve_internal_border - print a border inside the table even if
  987. border is disabled (True or False)"""
  988. return self._preserve_internal_border
  989. @preserve_internal_border.setter
  990. def preserve_internal_border(self, val: bool) -> None:
  991. self._validate_option("preserve_internal_border", val)
  992. self._preserve_internal_border = val
  993. @property
  994. def hrules(self) -> HRuleStyle:
  995. """Controls printing of horizontal rules after rows
  996. Arguments:
  997. hrules - horizontal rules style. Allowed values: HRuleStyle"""
  998. return self._hrules
  999. @hrules.setter
  1000. def hrules(self, val: HRuleStyle) -> None:
  1001. self._validate_option("hrules", val)
  1002. self._hrules = val
  1003. @property
  1004. def vrules(self) -> VRuleStyle:
  1005. """Controls printing of vertical rules between columns
  1006. Arguments:
  1007. vrules - vertical rules style. Allowed values: VRuleStyle"""
  1008. return self._vrules
  1009. @vrules.setter
  1010. def vrules(self, val: VRuleStyle) -> None:
  1011. self._validate_option("vrules", val)
  1012. self._vrules = val
  1013. @property
  1014. def int_format(self) -> dict[str, str]:
  1015. """Controls formatting of integer data
  1016. Arguments:
  1017. int_format - integer format string"""
  1018. return self._int_format
  1019. @int_format.setter
  1020. def int_format(self, val: str | dict[str, str] | None) -> None:
  1021. if isinstance(val, str):
  1022. self._validate_option("int_format", val)
  1023. for field in self._field_names:
  1024. self._int_format[field] = val
  1025. elif isinstance(val, dict) and val:
  1026. for field, fval in val.items():
  1027. self._validate_option("int_format", fval)
  1028. self._int_format[field] = fval
  1029. else:
  1030. self._int_format = {}
  1031. @property
  1032. def float_format(self) -> dict[str, str]:
  1033. """Controls formatting of floating point data
  1034. Arguments:
  1035. float_format - floating point format string"""
  1036. return self._float_format
  1037. @float_format.setter
  1038. def float_format(self, val: str | dict[str, str] | None) -> None:
  1039. if isinstance(val, str):
  1040. self._validate_option("float_format", val)
  1041. for field in self._field_names:
  1042. self._float_format[field] = val
  1043. elif isinstance(val, dict) and val:
  1044. for field, fval in val.items():
  1045. self._validate_option("float_format", fval)
  1046. self._float_format[field] = fval
  1047. else:
  1048. self._float_format = {}
  1049. @property
  1050. def custom_format(self) -> dict[str, Callable[[str, Any], str]]:
  1051. """Controls formatting of any column using callable
  1052. Arguments:
  1053. custom_format - Dictionary of field_name and callable"""
  1054. return self._custom_format
  1055. @custom_format.setter
  1056. def custom_format(
  1057. self,
  1058. val: Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None,
  1059. ):
  1060. if val is None:
  1061. self._custom_format = {}
  1062. elif isinstance(val, dict):
  1063. for k, v in val.items():
  1064. self._validate_function(f"custom_value.{k}", v)
  1065. self._custom_format = val
  1066. elif hasattr(val, "__call__"):
  1067. self._validate_function("custom_value", val)
  1068. for field in self._field_names:
  1069. self._custom_format[field] = val
  1070. else:
  1071. msg = "The custom_format property need to be a dictionary or callable"
  1072. raise TypeError(msg)
  1073. @property
  1074. def padding_width(self) -> int:
  1075. """The number of empty spaces between a column's edge and its content
  1076. Arguments:
  1077. padding_width - number of spaces, must be a positive integer"""
  1078. return self._padding_width
  1079. @padding_width.setter
  1080. def padding_width(self, val: int) -> None:
  1081. self._validate_option("padding_width", val)
  1082. self._padding_width = val
  1083. @property
  1084. def left_padding_width(self) -> int | None:
  1085. """The number of empty spaces between a column's left edge and its content
  1086. Arguments:
  1087. left_padding - number of spaces, must be a positive integer"""
  1088. return self._left_padding_width
  1089. @left_padding_width.setter
  1090. def left_padding_width(self, val: int) -> None:
  1091. self._validate_option("left_padding_width", val)
  1092. self._left_padding_width = val
  1093. @property
  1094. def right_padding_width(self) -> int | None:
  1095. """The number of empty spaces between a column's right edge and its content
  1096. Arguments:
  1097. right_padding - number of spaces, must be a positive integer"""
  1098. return self._right_padding_width
  1099. @right_padding_width.setter
  1100. def right_padding_width(self, val: int) -> None:
  1101. self._validate_option("right_padding_width", val)
  1102. self._right_padding_width = val
  1103. @property
  1104. def vertical_char(self) -> str:
  1105. """The character used when printing table borders to draw vertical lines
  1106. Arguments:
  1107. vertical_char - single character string used to draw vertical lines"""
  1108. return self._vertical_char
  1109. @vertical_char.setter
  1110. def vertical_char(self, val: str) -> None:
  1111. val = str(val)
  1112. self._validate_option("vertical_char", val)
  1113. self._vertical_char = val
  1114. @property
  1115. def horizontal_char(self) -> str:
  1116. """The character used when printing table borders to draw horizontal lines
  1117. Arguments:
  1118. horizontal_char - single character string used to draw horizontal lines"""
  1119. return self._horizontal_char
  1120. @horizontal_char.setter
  1121. def horizontal_char(self, val: str) -> None:
  1122. val = str(val)
  1123. self._validate_option("horizontal_char", val)
  1124. self._horizontal_char = val
  1125. @property
  1126. def horizontal_align_char(self) -> str:
  1127. """The character used to indicate column alignment in horizontal lines
  1128. Arguments:
  1129. horizontal_align_char - single character string used to indicate alignment"""
  1130. return self._bottom_left_junction_char or self.junction_char
  1131. @horizontal_align_char.setter
  1132. def horizontal_align_char(self, val: str) -> None:
  1133. val = str(val)
  1134. self._validate_option("horizontal_align_char", val)
  1135. self._horizontal_align_char = val
  1136. @property
  1137. def junction_char(self) -> str:
  1138. """The character used when printing table borders to draw line junctions
  1139. Arguments:
  1140. junction_char - single character string used to draw line junctions"""
  1141. return self._junction_char
  1142. @junction_char.setter
  1143. def junction_char(self, val: str) -> None:
  1144. val = str(val)
  1145. self._validate_option("junction_char", val)
  1146. self._junction_char = val
  1147. @property
  1148. def top_junction_char(self) -> str:
  1149. """The character used when printing table borders to draw top line junctions
  1150. Arguments:
  1151. top_junction_char - single character string used to draw top line junctions"""
  1152. return self._top_junction_char or self.junction_char
  1153. @top_junction_char.setter
  1154. def top_junction_char(self, val: str) -> None:
  1155. val = str(val)
  1156. self._validate_option("top_junction_char", val)
  1157. self._top_junction_char = val
  1158. @property
  1159. def bottom_junction_char(self) -> str:
  1160. """The character used when printing table borders to draw bottom line junctions
  1161. Arguments:
  1162. bottom_junction_char -
  1163. single character string used to draw bottom line junctions"""
  1164. return self._bottom_junction_char or self.junction_char
  1165. @bottom_junction_char.setter
  1166. def bottom_junction_char(self, val: str) -> None:
  1167. val = str(val)
  1168. self._validate_option("bottom_junction_char", val)
  1169. self._bottom_junction_char = val
  1170. @property
  1171. def right_junction_char(self) -> str:
  1172. """The character used when printing table borders to draw right line junctions
  1173. Arguments:
  1174. right_junction_char -
  1175. single character string used to draw right line junctions"""
  1176. return self._right_junction_char or self.junction_char
  1177. @right_junction_char.setter
  1178. def right_junction_char(self, val: str) -> None:
  1179. val = str(val)
  1180. self._validate_option("right_junction_char", val)
  1181. self._right_junction_char = val
  1182. @property
  1183. def left_junction_char(self) -> str:
  1184. """The character used when printing table borders to draw left line junctions
  1185. Arguments:
  1186. left_junction_char - single character string used to draw left line junctions"""
  1187. return self._left_junction_char or self.junction_char
  1188. @left_junction_char.setter
  1189. def left_junction_char(self, val: str) -> None:
  1190. val = str(val)
  1191. self._validate_option("left_junction_char", val)
  1192. self._left_junction_char = val
  1193. @property
  1194. def top_right_junction_char(self) -> str:
  1195. """
  1196. The character used when printing table borders to draw top-right line junctions
  1197. Arguments:
  1198. top_right_junction_char -
  1199. single character string used to draw top-right line junctions"""
  1200. return self._top_right_junction_char or self.junction_char
  1201. @top_right_junction_char.setter
  1202. def top_right_junction_char(self, val: str) -> None:
  1203. val = str(val)
  1204. self._validate_option("top_right_junction_char", val)
  1205. self._top_right_junction_char = val
  1206. @property
  1207. def top_left_junction_char(self) -> str:
  1208. """
  1209. The character used when printing table borders to draw top-left line junctions
  1210. Arguments:
  1211. top_left_junction_char -
  1212. single character string used to draw top-left line junctions"""
  1213. return self._top_left_junction_char or self.junction_char
  1214. @top_left_junction_char.setter
  1215. def top_left_junction_char(self, val: str) -> None:
  1216. val = str(val)
  1217. self._validate_option("top_left_junction_char", val)
  1218. self._top_left_junction_char = val
  1219. @property
  1220. def bottom_right_junction_char(self) -> str:
  1221. """The character used when printing table borders
  1222. to draw bottom-right line junctions
  1223. Arguments:
  1224. bottom_right_junction_char -
  1225. single character string used to draw bottom-right line junctions"""
  1226. return self._bottom_right_junction_char or self.junction_char
  1227. @bottom_right_junction_char.setter
  1228. def bottom_right_junction_char(self, val: str) -> None:
  1229. val = str(val)
  1230. self._validate_option("bottom_right_junction_char", val)
  1231. self._bottom_right_junction_char = val
  1232. @property
  1233. def bottom_left_junction_char(self) -> str:
  1234. """The character used when printing table borders
  1235. to draw bottom-left line junctions
  1236. Arguments:
  1237. bottom_left_junction_char -
  1238. single character string used to draw bottom-left line junctions"""
  1239. return self._bottom_left_junction_char or self.junction_char
  1240. @bottom_left_junction_char.setter
  1241. def bottom_left_junction_char(self, val: str) -> None:
  1242. val = str(val)
  1243. self._validate_option("bottom_left_junction_char", val)
  1244. self._bottom_left_junction_char = val
  1245. @property
  1246. def format(self) -> bool:
  1247. """Controls whether or not HTML tables are formatted to match styling options
  1248. Arguments:
  1249. format - True or False"""
  1250. return self._format
  1251. @format.setter
  1252. def format(self, val: bool) -> None:
  1253. self._validate_option("format", val)
  1254. self._format = val
  1255. @property
  1256. def print_empty(self) -> bool:
  1257. """Controls whether or not empty tables produce a header and frame or just an
  1258. empty string
  1259. Arguments:
  1260. print_empty - True or False"""
  1261. return self._print_empty
  1262. @print_empty.setter
  1263. def print_empty(self, val: bool) -> None:
  1264. self._validate_option("print_empty", val)
  1265. self._print_empty = val
  1266. @property
  1267. def attributes(self) -> dict[str, str]:
  1268. """A dictionary of HTML attribute name/value pairs to be included in the
  1269. <table> tag when printing HTML
  1270. Arguments:
  1271. attributes - dictionary of attributes"""
  1272. return self._attributes
  1273. @attributes.setter
  1274. def attributes(self, val: dict[str, str]) -> None:
  1275. self._validate_option("attributes", val)
  1276. self._attributes = val
  1277. @property
  1278. def oldsortslice(self) -> bool:
  1279. """oldsortslice - Slice rows before sorting in the "old style" """
  1280. return self._oldsortslice
  1281. @oldsortslice.setter
  1282. def oldsortslice(self, val: bool) -> None:
  1283. self._validate_option("oldsortslice", val)
  1284. self._oldsortslice = val
  1285. @property
  1286. def escape_header(self) -> bool:
  1287. """Escapes the text within a header (True or False)"""
  1288. return self._escape_header
  1289. @escape_header.setter
  1290. def escape_header(self, val: bool) -> None:
  1291. self._validate_option("escape_header", val)
  1292. self._escape_header = val
  1293. @property
  1294. def escape_data(self) -> bool:
  1295. """Escapes the text within a data field (True or False)"""
  1296. return self._escape_data
  1297. @escape_data.setter
  1298. def escape_data(self, val: bool) -> None:
  1299. self._validate_option("escape_data", val)
  1300. self._escape_data = val
  1301. @property
  1302. def break_on_hyphens(self) -> bool:
  1303. """Break longlines on hyphens (True or False)"""
  1304. return self._break_on_hyphens
  1305. @break_on_hyphens.setter
  1306. def break_on_hyphens(self, val: bool) -> None:
  1307. self._validate_option("break_on_hyphens", val)
  1308. self._break_on_hyphens = val
  1309. ##############################
  1310. # OPTION MIXER #
  1311. ##############################
  1312. def _get_options(self, kwargs: Mapping[str, Any]) -> OptionsType:
  1313. options: dict[str, Any] = {}
  1314. for option in self._options:
  1315. if option in kwargs:
  1316. self._validate_option(option, kwargs[option])
  1317. options[option] = kwargs[option]
  1318. else:
  1319. options[option] = getattr(self, option)
  1320. return cast(OptionsType, options)
  1321. ##############################
  1322. # PRESET STYLE LOGIC #
  1323. ##############################
  1324. def set_style(self, style: TableStyle) -> None:
  1325. self._set_default_style()
  1326. self._style = style
  1327. if style == TableStyle.MSWORD_FRIENDLY:
  1328. self._set_msword_style()
  1329. elif style == TableStyle.PLAIN_COLUMNS:
  1330. self._set_columns_style()
  1331. elif style == TableStyle.MARKDOWN:
  1332. self._set_markdown_style()
  1333. elif style == TableStyle.ORGMODE:
  1334. self._set_orgmode_style()
  1335. elif style == TableStyle.DOUBLE_BORDER:
  1336. self._set_double_border_style()
  1337. elif style == TableStyle.SINGLE_BORDER:
  1338. self._set_single_border_style()
  1339. elif style == TableStyle.RANDOM:
  1340. self._set_random_style()
  1341. elif style != TableStyle.DEFAULT:
  1342. msg = "Invalid pre-set style"
  1343. raise ValueError(msg)
  1344. def _set_orgmode_style(self) -> None:
  1345. self.orgmode = True
  1346. def _set_markdown_style(self) -> None:
  1347. self.header = True
  1348. self.border = True
  1349. self._hrules = HRuleStyle.HEADER
  1350. self.padding_width = 1
  1351. self.left_padding_width = 1
  1352. self.right_padding_width = 1
  1353. self.vertical_char = "|"
  1354. self.junction_char = "|"
  1355. self._horizontal_align_char = ":"
  1356. def _set_default_style(self) -> None:
  1357. self.header = True
  1358. self.border = True
  1359. self._hrules = HRuleStyle.FRAME
  1360. self._vrules = VRuleStyle.ALL
  1361. self.padding_width = 1
  1362. self.left_padding_width = 1
  1363. self.right_padding_width = 1
  1364. self.vertical_char = "|"
  1365. self.horizontal_char = "-"
  1366. self._horizontal_align_char = None
  1367. self.junction_char = "+"
  1368. self._top_junction_char = None
  1369. self._bottom_junction_char = None
  1370. self._right_junction_char = None
  1371. self._left_junction_char = None
  1372. self._top_right_junction_char = None
  1373. self._top_left_junction_char = None
  1374. self._bottom_right_junction_char = None
  1375. self._bottom_left_junction_char = None
  1376. def _set_msword_style(self) -> None:
  1377. self.header = True
  1378. self.border = True
  1379. self._hrules = HRuleStyle.NONE
  1380. self.padding_width = 1
  1381. self.left_padding_width = 1
  1382. self.right_padding_width = 1
  1383. self.vertical_char = "|"
  1384. def _set_columns_style(self) -> None:
  1385. self.header = True
  1386. self.border = False
  1387. self.padding_width = 1
  1388. self.left_padding_width = 0
  1389. self.right_padding_width = 8
  1390. def _set_double_border_style(self) -> None:
  1391. self.horizontal_char = "═"
  1392. self.vertical_char = "║"
  1393. self.junction_char = "╬"
  1394. self.top_junction_char = "╦"
  1395. self.bottom_junction_char = "╩"
  1396. self.right_junction_char = "╣"
  1397. self.left_junction_char = "╠"
  1398. self.top_right_junction_char = "╗"
  1399. self.top_left_junction_char = "╔"
  1400. self.bottom_right_junction_char = "╝"
  1401. self.bottom_left_junction_char = "╚"
  1402. def _set_single_border_style(self) -> None:
  1403. self.horizontal_char = "─"
  1404. self.vertical_char = "│"
  1405. self.junction_char = "┼"
  1406. self.top_junction_char = "┬"
  1407. self.bottom_junction_char = "┴"
  1408. self.right_junction_char = "┤"
  1409. self.left_junction_char = "├"
  1410. self.top_right_junction_char = "┐"
  1411. self.top_left_junction_char = "┌"
  1412. self.bottom_right_junction_char = "┘"
  1413. self.bottom_left_junction_char = "└"
  1414. def _set_random_style(self) -> None:
  1415. # Just for fun!
  1416. import random
  1417. self.header = random.choice((True, False))
  1418. self.border = random.choice((True, False))
  1419. self._hrules = random.choice(list(HRuleStyle))
  1420. self._vrules = random.choice(list(VRuleStyle))
  1421. self.left_padding_width = random.randint(0, 5)
  1422. self.right_padding_width = random.randint(0, 5)
  1423. self.vertical_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
  1424. self.horizontal_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
  1425. self.junction_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
  1426. self.preserve_internal_border = random.choice((True, False))
  1427. ##############################
  1428. # DATA INPUT METHODS #
  1429. ##############################
  1430. def add_rows(self, rows: Sequence[RowType], *, divider: bool = False) -> None:
  1431. """Add rows to the table
  1432. Arguments:
  1433. rows - rows of data, should be an iterable of lists, each list with as many
  1434. elements as the table has fields
  1435. divider - add row divider after the row block
  1436. """
  1437. for row in rows[:-1]:
  1438. self.add_row(row)
  1439. if len(rows) > 0:
  1440. self.add_row(rows[-1], divider=divider)
  1441. def add_row(self, row: RowType, *, divider: bool = False) -> None:
  1442. """Add a row to the table
  1443. Arguments:
  1444. row - row of data, should be a list with as many elements as the table
  1445. has fields"""
  1446. if self._field_names and len(row) != len(self._field_names):
  1447. msg = (
  1448. "Row has incorrect number of values, "
  1449. f"(actual) {len(row)}!={len(self._field_names)} (expected)"
  1450. )
  1451. raise ValueError(msg)
  1452. if not self._field_names:
  1453. self.field_names = [f"Field {n + 1}" for n in range(len(row))]
  1454. self._rows.append(list(row))
  1455. self._dividers.append(divider)
  1456. def del_row(self, row_index: int) -> None:
  1457. """Delete a row from the table
  1458. Arguments:
  1459. row_index - The index of the row you want to delete. Indexing starts at 0."""
  1460. if row_index > len(self._rows) - 1:
  1461. msg = (
  1462. f"Can't delete row at index {row_index}, "
  1463. f"table only has {len(self._rows)} rows"
  1464. )
  1465. raise IndexError(msg)
  1466. del self._rows[row_index]
  1467. del self._dividers[row_index]
  1468. def add_divider(self) -> None:
  1469. """Add a divider to the table"""
  1470. if len(self._dividers) >= 1:
  1471. self._dividers[-1] = True
  1472. def add_column(
  1473. self,
  1474. fieldname: str,
  1475. column: Sequence[Any],
  1476. align: AlignType = "c",
  1477. valign: VAlignType = "t",
  1478. ) -> None:
  1479. """Add a column to the table.
  1480. Arguments:
  1481. fieldname - name of the field to contain the new column of data
  1482. column - column of data, should be a list with as many elements as the
  1483. table has rows
  1484. align - desired alignment for this column - "l" for left, "c" for centre and
  1485. "r" for right
  1486. valign - desired vertical alignment for new columns - "t" for top,
  1487. "m" for middle and "b" for bottom"""
  1488. if len(self._rows) in (0, len(column)):
  1489. self._validate_align(align)
  1490. self._validate_valign(valign)
  1491. self._field_names.append(fieldname)
  1492. self._align[fieldname] = align
  1493. self._valign[fieldname] = valign
  1494. for i in range(len(column)):
  1495. if len(self._rows) < i + 1:
  1496. self._rows.append([])
  1497. self._dividers.append(False)
  1498. self._rows[i].append(column[i])
  1499. else:
  1500. msg = (
  1501. f"Column length {len(column)} does not match number of rows "
  1502. f"{len(self._rows)}"
  1503. )
  1504. raise ValueError(msg)
  1505. def add_autoindex(self, fieldname: str = "Index") -> None:
  1506. """Add an auto-incrementing index column to the table.
  1507. Arguments:
  1508. fieldname - name of the field to contain the new column of data"""
  1509. self._field_names.insert(0, fieldname)
  1510. self._align[fieldname] = self._kwargs["align"] or "c"
  1511. self._valign[fieldname] = self._kwargs["valign"] or "t"
  1512. for i, row in enumerate(self._rows):
  1513. row.insert(0, i + 1)
  1514. def del_column(self, fieldname: str) -> None:
  1515. """Delete a column from the table
  1516. Arguments:
  1517. fieldname - The field name of the column you want to delete."""
  1518. if fieldname not in self._field_names:
  1519. msg = (
  1520. "Can't delete column {!r} which is not a field name of this table."
  1521. " Field names are: {}".format(
  1522. fieldname, ", ".join(map(repr, self._field_names))
  1523. )
  1524. )
  1525. raise ValueError(msg)
  1526. col_index = self._field_names.index(fieldname)
  1527. del self._field_names[col_index]
  1528. for row in self._rows:
  1529. del row[col_index]
  1530. def clear_rows(self) -> None:
  1531. """Delete all rows from the table but keep the current field names"""
  1532. self._rows = []
  1533. self._dividers = []
  1534. def clear(self) -> None:
  1535. """Delete all rows and field names from the table, maintaining nothing but
  1536. styling options"""
  1537. self._rows = []
  1538. self._dividers = []
  1539. self._field_names = []
  1540. self._widths = []
  1541. ##############################
  1542. # MISC PUBLIC METHODS #
  1543. ##############################
  1544. def copy(self) -> Self:
  1545. import copy
  1546. return copy.deepcopy(self)
  1547. def get_formatted_string(self, out_format: str = "text", **kwargs) -> str:
  1548. """Return string representation of specified format of table in current state.
  1549. Arguments:
  1550. out_format - resulting table format
  1551. kwargs - passed through to function that performs formatting
  1552. """
  1553. if out_format == "text":
  1554. return self.get_string(**kwargs)
  1555. if out_format == "html":
  1556. return self.get_html_string(**kwargs)
  1557. if out_format == "json":
  1558. return self.get_json_string(**kwargs)
  1559. if out_format == "csv":
  1560. return self.get_csv_string(**kwargs)
  1561. if out_format == "latex":
  1562. return self.get_latex_string(**kwargs)
  1563. if out_format == "mediawiki":
  1564. return self.get_mediawiki_string(**kwargs)
  1565. msg = (
  1566. f"Invalid format {out_format}. "
  1567. "Must be one of: text, html, json, csv, latex or mediawiki"
  1568. )
  1569. raise ValueError(msg)
  1570. ##############################
  1571. # MISC PRIVATE METHODS #
  1572. ##############################
  1573. def _format_value(self, field: str, value: Any) -> str:
  1574. if isinstance(value, int) and field in self._int_format:
  1575. return (f"%{self._int_format[field]}d") % value
  1576. elif isinstance(value, float) and field in self._float_format:
  1577. return (f"%{self._float_format[field]}f") % value
  1578. formatter = self._custom_format.get(field, (lambda f, v: str(v)))
  1579. return formatter(field, value)
  1580. def _compute_table_width(self, options) -> int:
  1581. if options["vrules"] == VRuleStyle.FRAME:
  1582. table_width = 2
  1583. if options["vrules"] == VRuleStyle.ALL:
  1584. table_width = 1
  1585. else:
  1586. table_width = 0
  1587. per_col_padding = sum(self._get_padding_widths(options))
  1588. for index, fieldname in enumerate(self.field_names):
  1589. if not options["fields"] or (
  1590. options["fields"] and fieldname in options["fields"]
  1591. ):
  1592. table_width += self._widths[index] + per_col_padding + 1
  1593. return table_width
  1594. def _compute_widths(self, rows: list[list[str]], options: OptionsType) -> None:
  1595. if options["header"] and options["use_header_width"]:
  1596. widths = [_get_size(field)[0] for field in self._field_names]
  1597. else:
  1598. widths = len(self.field_names) * [0]
  1599. for row in rows:
  1600. for index, value in enumerate(row):
  1601. fieldname = self.field_names[index]
  1602. if (
  1603. value == "None"
  1604. and (none_val := self.none_format.get(fieldname)) is not None
  1605. ):
  1606. value = none_val
  1607. if fieldname in self.max_width:
  1608. widths[index] = max(
  1609. widths[index],
  1610. min(_get_size(value)[0], self.max_width[fieldname]),
  1611. )
  1612. else:
  1613. widths[index] = max(widths[index], _get_size(value)[0])
  1614. if fieldname in self.min_width:
  1615. widths[index] = max(widths[index], self.min_width[fieldname])
  1616. if self._style == TableStyle.MARKDOWN:
  1617. # Markdown needs at least one hyphen in the divider
  1618. if self._align[fieldname] in ("l", "r"):
  1619. min_width = 1
  1620. else: # "c"
  1621. min_width = 3
  1622. widths[index] = max(min_width, widths[index])
  1623. self._widths = widths
  1624. per_col_padding = sum(self._get_padding_widths(options))
  1625. # Are we exceeding max_table_width?
  1626. if self._max_table_width:
  1627. table_width = self._compute_table_width(options)
  1628. if table_width > self._max_table_width:
  1629. # Shrink widths in proportion
  1630. markup_chars = per_col_padding * len(widths) + len(widths) - 1
  1631. scale = (self._max_table_width - markup_chars) / (
  1632. table_width - markup_chars
  1633. )
  1634. self._widths = [max(1, int(w * scale)) for w in widths]
  1635. # Are we under min_table_width or title width?
  1636. if self._min_table_width or options["title"]:
  1637. if options["title"]:
  1638. title_width = _str_block_width(options["title"]) + per_col_padding
  1639. if options["vrules"] in (VRuleStyle.FRAME, VRuleStyle.ALL):
  1640. title_width += 2
  1641. else:
  1642. title_width = 0
  1643. min_table_width = self.min_table_width or 0
  1644. min_width = max(title_width, min_table_width)
  1645. if options["border"]:
  1646. borders = len(widths) + 1
  1647. elif options["preserve_internal_border"]:
  1648. borders = len(widths)
  1649. else:
  1650. borders = 0
  1651. # Subtract padding for each column and borders
  1652. min_width -= sum([per_col_padding for _ in widths]) + borders
  1653. # What is being scaled is content so we sum column widths
  1654. content_width = sum(widths) or 1
  1655. if content_width < min_width:
  1656. # Grow widths in proportion
  1657. scale = 1.0 * min_width / content_width
  1658. widths = [int(w * scale) for w in widths]
  1659. if sum(widths) < min_width:
  1660. widths[-1] += min_width - sum(widths)
  1661. self._widths = widths
  1662. def _get_padding_widths(self, options: OptionsType) -> tuple[int, int]:
  1663. if options["left_padding_width"] is not None:
  1664. lpad = options["left_padding_width"]
  1665. else:
  1666. lpad = options["padding_width"]
  1667. if options["right_padding_width"] is not None:
  1668. rpad = options["right_padding_width"]
  1669. else:
  1670. rpad = options["padding_width"]
  1671. return lpad, rpad
  1672. def _get_rows(self, options: OptionsType) -> list[RowType]:
  1673. """Return only those data rows that should be printed, based on slicing and
  1674. sorting.
  1675. Arguments:
  1676. options - dictionary of option settings."""
  1677. if options["oldsortslice"]:
  1678. rows = self._rows[options["start"] : options["end"]]
  1679. else:
  1680. rows = self._rows
  1681. rows = [row for row in rows if options["row_filter"](row)]
  1682. # Sort
  1683. if options["sortby"]:
  1684. sortindex = self._field_names.index(options["sortby"])
  1685. # Decorate
  1686. rows = [[row[sortindex]] + row for row in rows]
  1687. # Sort
  1688. rows.sort(reverse=options["reversesort"], key=options["sort_key"])
  1689. # Undecorate
  1690. rows = [row[1:] for row in rows]
  1691. # Slice if necessary
  1692. if not options["oldsortslice"]:
  1693. rows = rows[options["start"] : options["end"]]
  1694. return rows
  1695. def _get_dividers(self, options: OptionsType) -> list[bool]:
  1696. """Return only those dividers that should be printed, based on slicing.
  1697. Arguments:
  1698. options - dictionary of option settings."""
  1699. if options["oldsortslice"]:
  1700. dividers = self._dividers[options["start"] : options["end"]]
  1701. else:
  1702. dividers = self._dividers
  1703. if options["sortby"]:
  1704. dividers = [False for divider in dividers]
  1705. return dividers
  1706. def _format_row(self, row: RowType) -> list[str]:
  1707. return [
  1708. self._format_value(field, value)
  1709. for (field, value) in zip(self._field_names, row)
  1710. ]
  1711. def _format_rows(self, rows: list[RowType]) -> list[list[str]]:
  1712. return [self._format_row(row) for row in rows]
  1713. ##############################
  1714. # PLAIN TEXT STRING METHODS #
  1715. ##############################
  1716. def get_string(self, **kwargs) -> str:
  1717. """Return string representation of table in current state.
  1718. Arguments:
  1719. title - optional table title
  1720. start - index of first data row to include in output
  1721. end - index of last data row to include in output PLUS ONE (list slice style)
  1722. fields - names of fields (columns) to include
  1723. header - print a header showing field names (True or False)
  1724. use_header_width - reflect width of header (True or False)
  1725. border - print a border around the table (True or False)
  1726. preserve_internal_border - print a border inside the table even if
  1727. border is disabled (True or False)
  1728. hrules - controls printing of horizontal rules after rows.
  1729. Allowed values: HRuleStyle
  1730. vrules - controls printing of vertical rules between columns.
  1731. Allowed values: VRuleStyle
  1732. int_format - controls formatting of integer data
  1733. float_format - controls formatting of floating point data
  1734. custom_format - controls formatting of any column using callable
  1735. padding_width - number of spaces on either side of column data (only used if
  1736. left and right paddings are None)
  1737. left_padding_width - number of spaces on left hand side of column data
  1738. right_padding_width - number of spaces on right hand side of column data
  1739. vertical_char - single character string used to draw vertical lines
  1740. horizontal_char - single character string used to draw horizontal lines
  1741. horizontal_align_char - single character string used to indicate alignment
  1742. junction_char - single character string used to draw line junctions
  1743. junction_char - single character string used to draw line junctions
  1744. top_junction_char - single character string used to draw top line junctions
  1745. bottom_junction_char -
  1746. single character string used to draw bottom line junctions
  1747. right_junction_char - single character string used to draw right line junctions
  1748. left_junction_char - single character string used to draw left line junctions
  1749. top_right_junction_char -
  1750. single character string used to draw top-right line junctions
  1751. top_left_junction_char -
  1752. single character string used to draw top-left line junctions
  1753. bottom_right_junction_char -
  1754. single character string used to draw bottom-right line junctions
  1755. bottom_left_junction_char -
  1756. single character string used to draw bottom-left line junctions
  1757. sortby - name of field to sort rows by
  1758. sort_key - sorting key function, applied to data points before sorting
  1759. reversesort - True or False to sort in descending or ascending order
  1760. row_filter - filter function applied on rows
  1761. print empty - if True, stringify just the header for an empty table,
  1762. if False return an empty string"""
  1763. options = self._get_options(kwargs)
  1764. lines: list[str] = []
  1765. # Don't think too hard about an empty table
  1766. # Is this the desired behaviour? Maybe we should still print the header?
  1767. if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
  1768. return ""
  1769. # Get the rows we need to print, taking into account slicing, sorting, etc.
  1770. rows = self._get_rows(options)
  1771. dividers = self._get_dividers(options)
  1772. # Turn all data in all rows into Unicode, formatted as desired
  1773. formatted_rows = self._format_rows(rows)
  1774. # Compute column widths
  1775. self._compute_widths(formatted_rows, options)
  1776. self._hrule = self._stringify_hrule(options)
  1777. # Add title
  1778. title = options["title"] or self._title
  1779. if title:
  1780. lines.append(self._stringify_title(title, options))
  1781. # Add header or top of border
  1782. if options["header"]:
  1783. lines.append(self._stringify_header(options))
  1784. elif options["border"] and options["hrules"] in (
  1785. HRuleStyle.ALL,
  1786. HRuleStyle.FRAME,
  1787. ):
  1788. lines.append(self._stringify_hrule(options, where="top_"))
  1789. if title and options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1790. left_j_len = len(self.left_junction_char)
  1791. right_j_len = len(self.right_junction_char)
  1792. lines[-1] = (
  1793. self.left_junction_char
  1794. + lines[-1][left_j_len:-right_j_len]
  1795. + self.right_junction_char
  1796. )
  1797. # Add rows
  1798. for row, divider in zip(formatted_rows[:-1], dividers[:-1]):
  1799. lines.append(self._stringify_row(row, options, self._hrule))
  1800. if divider:
  1801. lines.append(self._stringify_hrule(options))
  1802. if formatted_rows:
  1803. lines.append(
  1804. self._stringify_row(
  1805. formatted_rows[-1],
  1806. options,
  1807. self._stringify_hrule(options, where="bottom_"),
  1808. )
  1809. )
  1810. # Add bottom of border
  1811. if options["border"] and options["hrules"] == HRuleStyle.FRAME:
  1812. lines.append(self._stringify_hrule(options, where="bottom_"))
  1813. if "orgmode" in self.__dict__ and self.orgmode:
  1814. left_j_len = len(self.left_junction_char)
  1815. right_j_len = len(self.right_junction_char)
  1816. lines = [
  1817. "|" + new_line[left_j_len:-right_j_len] + "|"
  1818. for old_line in lines
  1819. for new_line in old_line.split("\n")
  1820. ]
  1821. return "\n".join(lines)
  1822. def _stringify_hrule(
  1823. self, options: OptionsType, where: Literal["top_", "bottom_", ""] = ""
  1824. ) -> str:
  1825. if not options["border"] and not options["preserve_internal_border"]:
  1826. return ""
  1827. lpad, rpad = self._get_padding_widths(options)
  1828. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1829. bits = [options[where + "left_junction_char"]] # type: ignore[literal-required]
  1830. else:
  1831. bits = [options["horizontal_char"]]
  1832. # For tables with no data or fieldnames
  1833. if not self._field_names:
  1834. bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
  1835. return "".join(bits)
  1836. for field, width in zip(self._field_names, self._widths):
  1837. if options["fields"] and field not in options["fields"]:
  1838. continue
  1839. line = (width + lpad + rpad) * options["horizontal_char"]
  1840. # If necessary, add column alignment characters (e.g. ":" for Markdown)
  1841. if self._horizontal_align_char:
  1842. if self._align[field] in ("l", "c"):
  1843. line = " " + self._horizontal_align_char + line[2:]
  1844. if self._align[field] in ("c", "r"):
  1845. line = line[:-2] + self._horizontal_align_char + " "
  1846. bits.append(line)
  1847. if options["vrules"] == VRuleStyle.ALL:
  1848. bits.append(options[where + "junction_char"]) # type: ignore[literal-required]
  1849. else:
  1850. bits.append(options["horizontal_char"])
  1851. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1852. bits.pop()
  1853. bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
  1854. if options["preserve_internal_border"] and not options["border"]:
  1855. bits = bits[1:-1]
  1856. return "".join(bits)
  1857. def _stringify_title(self, title: str, options: OptionsType) -> str:
  1858. lines: list[str] = []
  1859. lpad, rpad = self._get_padding_widths(options)
  1860. if options["border"]:
  1861. if options["vrules"] == VRuleStyle.ALL:
  1862. options["vrules"] = VRuleStyle.FRAME
  1863. lines.append(self._stringify_hrule(options, "top_"))
  1864. options["vrules"] = VRuleStyle.ALL
  1865. elif options["vrules"] == VRuleStyle.FRAME:
  1866. lines.append(self._stringify_hrule(options, "top_"))
  1867. bits: list[str] = []
  1868. endpoint = (
  1869. options["vertical_char"]
  1870. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME)
  1871. and options["border"]
  1872. else " "
  1873. )
  1874. bits.append(endpoint)
  1875. title = " " * lpad + title + " " * rpad
  1876. lpad, rpad = self._get_padding_widths(options)
  1877. sum_widths = sum([n + lpad + rpad + 1 for n in self._widths])
  1878. bits.append(self._justify(title, sum_widths - 1, "c"))
  1879. bits.append(endpoint)
  1880. lines.append("".join(bits))
  1881. return "\n".join(lines)
  1882. def _stringify_header(self, options: OptionsType) -> str:
  1883. bits: list[str] = []
  1884. lpad, rpad = self._get_padding_widths(options)
  1885. if options["border"]:
  1886. if options["hrules"] in (HRuleStyle.ALL, HRuleStyle.FRAME):
  1887. bits.append(self._stringify_hrule(options, "top_"))
  1888. if options["title"] and options["vrules"] in (
  1889. VRuleStyle.ALL,
  1890. VRuleStyle.FRAME,
  1891. ):
  1892. left_j_len = len(self.left_junction_char)
  1893. right_j_len = len(self.right_junction_char)
  1894. bits[-1] = (
  1895. self.left_junction_char
  1896. + bits[-1][left_j_len:-right_j_len]
  1897. + self.right_junction_char
  1898. )
  1899. bits.append("\n")
  1900. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1901. bits.append(options["vertical_char"])
  1902. else:
  1903. bits.append(" ")
  1904. # For tables with no data or field names
  1905. if not self._field_names:
  1906. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1907. bits.append(options["vertical_char"])
  1908. else:
  1909. bits.append(" ")
  1910. for field, width in zip(self._field_names, self._widths):
  1911. if options["fields"] and field not in options["fields"]:
  1912. continue
  1913. if self._header_style == "cap":
  1914. fieldname = field.capitalize()
  1915. elif self._header_style == "title":
  1916. fieldname = field.title()
  1917. elif self._header_style == "upper":
  1918. fieldname = field.upper()
  1919. elif self._header_style == "lower":
  1920. fieldname = field.lower()
  1921. else:
  1922. fieldname = field
  1923. if _str_block_width(fieldname) > width:
  1924. fieldname = fieldname[:width]
  1925. bits.append(
  1926. " " * lpad
  1927. + self._justify(fieldname, width, self._align[field])
  1928. + " " * rpad
  1929. )
  1930. if options["border"] or options["preserve_internal_border"]:
  1931. if options["vrules"] == VRuleStyle.ALL:
  1932. bits.append(options["vertical_char"])
  1933. else:
  1934. bits.append(" ")
  1935. # If only preserve_internal_border is true, then we just appended
  1936. # a vertical character at the end when we wanted a space
  1937. if not options["border"] and options["preserve_internal_border"]:
  1938. bits.pop()
  1939. bits.append(" ")
  1940. # If vrules is FRAME, then we just appended a space at the end
  1941. # of the last field, when we really want a vertical character
  1942. if options["border"] and options["vrules"] == VRuleStyle.FRAME:
  1943. bits.pop()
  1944. bits.append(options["vertical_char"])
  1945. if (options["border"] or options["preserve_internal_border"]) and options[
  1946. "hrules"
  1947. ] != HRuleStyle.NONE:
  1948. bits.append("\n")
  1949. bits.append(self._hrule)
  1950. return "".join(bits)
  1951. def _stringify_row(self, row: list[str], options: OptionsType, hrule: str) -> str:
  1952. import textwrap
  1953. for index, field, value, width in zip(
  1954. range(len(row)), self._field_names, row, self._widths
  1955. ):
  1956. # Enforce max widths
  1957. lines = value.split("\n")
  1958. new_lines: list[str] = []
  1959. for line in lines:
  1960. if (
  1961. line == "None"
  1962. and (none_val := self.none_format.get(field)) is not None
  1963. ):
  1964. line = none_val
  1965. if _str_block_width(line) > width:
  1966. line = textwrap.fill(
  1967. line, width, break_on_hyphens=options["break_on_hyphens"]
  1968. )
  1969. new_lines.append(line)
  1970. lines = new_lines
  1971. value = "\n".join(lines)
  1972. row[index] = value
  1973. row_height = 0
  1974. for c in row:
  1975. h = _get_size(c)[1]
  1976. if h > row_height:
  1977. row_height = h
  1978. bits: list[list[str]] = []
  1979. lpad, rpad = self._get_padding_widths(options)
  1980. for y in range(row_height):
  1981. bits.append([])
  1982. if options["border"]:
  1983. if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
  1984. bits[y].append(self.vertical_char)
  1985. else:
  1986. bits[y].append(" ")
  1987. for field, value, width in zip(self._field_names, row, self._widths):
  1988. valign = self._valign[field]
  1989. lines = value.split("\n")
  1990. d_height = row_height - len(lines)
  1991. if d_height:
  1992. if valign == "m":
  1993. lines = (
  1994. [""] * int(d_height / 2)
  1995. + lines
  1996. + [""] * (d_height - int(d_height / 2))
  1997. )
  1998. elif valign == "b":
  1999. lines = [""] * d_height + lines
  2000. else:
  2001. lines = lines + [""] * d_height
  2002. for y, line in enumerate(lines):
  2003. if options["fields"] and field not in options["fields"]:
  2004. continue
  2005. bits[y].append(
  2006. " " * lpad
  2007. + self._justify(line, width, self._align[field])
  2008. + " " * rpad
  2009. )
  2010. if options["border"] or options["preserve_internal_border"]:
  2011. if options["vrules"] == VRuleStyle.ALL:
  2012. bits[y].append(self.vertical_char)
  2013. else:
  2014. bits[y].append(" ")
  2015. # If only preserve_internal_border is true, then we just appended
  2016. # a vertical character at the end when we wanted a space
  2017. if not options["border"] and options["preserve_internal_border"]:
  2018. bits[-1].pop()
  2019. bits[-1].append(" ")
  2020. # If vrules is FRAME, then we just appended a space at the end
  2021. # of the last field, when we really want a vertical character
  2022. for y in range(row_height):
  2023. if options["border"] and options["vrules"] == VRuleStyle.FRAME:
  2024. bits[y].pop()
  2025. bits[y].append(options["vertical_char"])
  2026. if options["border"] and options["hrules"] == HRuleStyle.ALL:
  2027. bits[row_height - 1].append("\n")
  2028. bits[row_height - 1].append(hrule)
  2029. bits_str = ["".join(bits_y) for bits_y in bits]
  2030. return "\n".join(bits_str)
  2031. def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs) -> str:
  2032. pages: list[str] = []
  2033. kwargs["start"] = kwargs.get("start", 0)
  2034. true_end = kwargs.get("end", self.rowcount)
  2035. while True:
  2036. kwargs["end"] = min(kwargs["start"] + page_length, true_end)
  2037. pages.append(self.get_string(**kwargs))
  2038. if kwargs["end"] == true_end:
  2039. break
  2040. kwargs["start"] += page_length
  2041. return line_break.join(pages)
  2042. ##############################
  2043. # CSV STRING METHODS #
  2044. ##############################
  2045. def get_csv_string(self, **kwargs) -> str:
  2046. """Return string representation of CSV formatted table in the current state
  2047. Keyword arguments are first interpreted as table formatting options, and
  2048. then any unused keyword arguments are passed to csv.writer(). For
  2049. example, get_csv_string(header=False, delimiter='\t') would use
  2050. header as a PrettyTable formatting option (skip the header row) and
  2051. delimiter as a csv.writer keyword argument.
  2052. """
  2053. import csv
  2054. options = self._get_options(kwargs)
  2055. csv_options = {
  2056. key: value for key, value in kwargs.items() if key not in options
  2057. }
  2058. csv_buffer = io.StringIO()
  2059. csv_writer = csv.writer(csv_buffer, **csv_options)
  2060. if options.get("header"):
  2061. if options["fields"]:
  2062. csv_writer.writerow(
  2063. [f for f in self._field_names if f in options["fields"]]
  2064. )
  2065. else:
  2066. csv_writer.writerow(self._field_names)
  2067. rows = self._get_rows(options)
  2068. if options["fields"]:
  2069. rows = [
  2070. [d for f, d in zip(self._field_names, row) if f in options["fields"]]
  2071. for row in rows
  2072. ]
  2073. for row in rows:
  2074. csv_writer.writerow(row)
  2075. return csv_buffer.getvalue()
  2076. ##############################
  2077. # JSON STRING METHODS #
  2078. ##############################
  2079. def get_json_string(self, **kwargs) -> str:
  2080. """Return string representation of JSON formatted table in the current state
  2081. Keyword arguments are first interpreted as table formatting options, and
  2082. then any unused keyword arguments are passed to json.dumps(). For
  2083. example, get_json_string(header=False, indent=2) would use header as
  2084. a PrettyTable formatting option (skip the header row) and indent as a
  2085. json.dumps keyword argument.
  2086. """
  2087. import json
  2088. options = self._get_options(kwargs)
  2089. json_options: dict[str, Any] = {
  2090. "indent": 4,
  2091. "separators": (",", ": "),
  2092. "sort_keys": True,
  2093. }
  2094. json_options.update(
  2095. {key: value for key, value in kwargs.items() if key not in options}
  2096. )
  2097. objects: list[list[str] | dict[str, Any]] = []
  2098. if options.get("header"):
  2099. if options["fields"]:
  2100. objects.append([f for f in self._field_names if f in options["fields"]])
  2101. else:
  2102. objects.append(self.field_names)
  2103. rows = self._get_rows(options)
  2104. if options["fields"]:
  2105. for row in rows:
  2106. objects.append(
  2107. {
  2108. f: d
  2109. for f, d in zip(self._field_names, row)
  2110. if f in options["fields"]
  2111. }
  2112. )
  2113. else:
  2114. for row in rows:
  2115. objects.append(dict(zip(self._field_names, row)))
  2116. return json.dumps(objects, **json_options)
  2117. ##############################
  2118. # HTML STRING METHODS #
  2119. ##############################
  2120. def get_html_string(self, **kwargs) -> str:
  2121. """Return string representation of HTML formatted version of table in current
  2122. state.
  2123. Arguments:
  2124. title - optional table title
  2125. start - index of first data row to include in output
  2126. end - index of last data row to include in output PLUS ONE (list slice style)
  2127. fields - names of fields (columns) to include
  2128. header - print a header showing field names (True or False)
  2129. escape_header - escapes the text within a header (True or False)
  2130. border - print a border around the table (True or False)
  2131. preserve_internal_border - print a border inside the table even if
  2132. border is disabled (True or False)
  2133. hrules - controls printing of horizontal rules after rows.
  2134. Allowed values: HRuleStyle
  2135. vrules - controls printing of vertical rules between columns.
  2136. Allowed values: VRuleStyle
  2137. int_format - controls formatting of integer data
  2138. float_format - controls formatting of floating point data
  2139. custom_format - controls formatting of any column using callable
  2140. padding_width - number of spaces on either side of column data (only used if
  2141. left and right paddings are None)
  2142. left_padding_width - number of spaces on left hand side of column data
  2143. right_padding_width - number of spaces on right hand side of column data
  2144. sortby - name of field to sort rows by
  2145. sort_key - sorting key function, applied to data points before sorting
  2146. row_filter - filter function applied on rows
  2147. attributes - dictionary of name/value pairs to include as HTML attributes in the
  2148. <table> tag
  2149. format - Controls whether or not HTML tables are formatted to match
  2150. styling options (True or False)
  2151. escape_data - escapes the text within a data field (True or False)
  2152. xhtml - print <br/> tags if True, <br> tags if False
  2153. break_on_hyphens - Whether long lines are broken on hypens or not, default: True
  2154. """
  2155. options = self._get_options(kwargs)
  2156. if options["format"]:
  2157. string = self._get_formatted_html_string(options)
  2158. else:
  2159. string = self._get_simple_html_string(options)
  2160. return string
  2161. def _get_simple_html_string(self, options: OptionsType) -> str:
  2162. from html import escape
  2163. lines: list[str] = []
  2164. if options["xhtml"]:
  2165. linebreak = "<br/>"
  2166. else:
  2167. linebreak = "<br>"
  2168. open_tag = ["<table"]
  2169. if options["attributes"]:
  2170. for attr_name, attr_value in options["attributes"].items():
  2171. open_tag.append(f' {escape(attr_name)}="{escape(attr_value)}"')
  2172. open_tag.append(">")
  2173. lines.append("".join(open_tag))
  2174. # Title
  2175. title = options["title"] or self._title
  2176. if title:
  2177. lines.append(f" <caption>{escape(title)}</caption>")
  2178. # Headers
  2179. if options["header"]:
  2180. lines.append(" <thead>")
  2181. lines.append(" <tr>")
  2182. for field in self._field_names:
  2183. if options["fields"] and field not in options["fields"]:
  2184. continue
  2185. if options["escape_header"]:
  2186. field = escape(field)
  2187. lines.append(
  2188. " <th>{}</th>".format(field.replace("\n", linebreak))
  2189. )
  2190. lines.append(" </tr>")
  2191. lines.append(" </thead>")
  2192. # Data
  2193. lines.append(" <tbody>")
  2194. rows = self._get_rows(options)
  2195. formatted_rows = self._format_rows(rows)
  2196. for row in formatted_rows:
  2197. lines.append(" <tr>")
  2198. for field, datum in zip(self._field_names, row):
  2199. if options["fields"] and field not in options["fields"]:
  2200. continue
  2201. if options["escape_data"]:
  2202. datum = escape(datum)
  2203. lines.append(
  2204. " <td>{}</td>".format(datum.replace("\n", linebreak))
  2205. )
  2206. lines.append(" </tr>")
  2207. lines.append(" </tbody>")
  2208. lines.append("</table>")
  2209. return "\n".join(lines)
  2210. def _get_formatted_html_string(self, options: OptionsType) -> str:
  2211. from html import escape
  2212. lines: list[str] = []
  2213. lpad, rpad = self._get_padding_widths(options)
  2214. if options["xhtml"]:
  2215. linebreak = "<br/>"
  2216. else:
  2217. linebreak = "<br>"
  2218. open_tag = ["<table"]
  2219. if options["border"]:
  2220. if (
  2221. options["hrules"] == HRuleStyle.ALL
  2222. and options["vrules"] == VRuleStyle.ALL
  2223. ):
  2224. open_tag.append(' frame="box" rules="all"')
  2225. elif (
  2226. options["hrules"] == HRuleStyle.FRAME
  2227. and options["vrules"] == VRuleStyle.FRAME
  2228. ):
  2229. open_tag.append(' frame="box"')
  2230. elif (
  2231. options["hrules"] == HRuleStyle.FRAME
  2232. and options["vrules"] == VRuleStyle.ALL
  2233. ):
  2234. open_tag.append(' frame="box" rules="cols"')
  2235. elif options["hrules"] == HRuleStyle.FRAME:
  2236. open_tag.append(' frame="hsides"')
  2237. elif options["hrules"] == HRuleStyle.ALL:
  2238. open_tag.append(' frame="hsides" rules="rows"')
  2239. elif options["vrules"] == VRuleStyle.FRAME:
  2240. open_tag.append(' frame="vsides"')
  2241. elif options["vrules"] == VRuleStyle.ALL:
  2242. open_tag.append(' frame="vsides" rules="cols"')
  2243. if not options["border"] and options["preserve_internal_border"]:
  2244. open_tag.append(' rules="cols"')
  2245. if options["attributes"]:
  2246. for attr_name, attr_value in options["attributes"].items():
  2247. open_tag.append(f' {escape(attr_name)}="{escape(attr_value)}"')
  2248. open_tag.append(">")
  2249. lines.append("".join(open_tag))
  2250. # Title
  2251. title = options["title"] or self._title
  2252. if title:
  2253. lines.append(f" <caption>{escape(title)}</caption>")
  2254. # Headers
  2255. if options["header"]:
  2256. lines.append(" <thead>")
  2257. lines.append(" <tr>")
  2258. for field in self._field_names:
  2259. if options["fields"] and field not in options["fields"]:
  2260. continue
  2261. if options["escape_header"]:
  2262. field = escape(field)
  2263. content = field.replace("\n", linebreak)
  2264. lines.append(
  2265. f' <th style="'
  2266. f"padding-left: {lpad}em; "
  2267. f"padding-right: {rpad}em; "
  2268. f'text-align: center">{content}</th>'
  2269. )
  2270. lines.append(" </tr>")
  2271. lines.append(" </thead>")
  2272. # Data
  2273. lines.append(" <tbody>")
  2274. rows = self._get_rows(options)
  2275. formatted_rows = self._format_rows(rows)
  2276. aligns: list[str] = []
  2277. valigns: list[str] = []
  2278. for field in self._field_names:
  2279. aligns.append(
  2280. {"l": "left", "r": "right", "c": "center"}[self._align[field]]
  2281. )
  2282. valigns.append(
  2283. {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
  2284. )
  2285. for row in formatted_rows:
  2286. lines.append(" <tr>")
  2287. for field, datum, align, valign in zip(
  2288. self._field_names, row, aligns, valigns
  2289. ):
  2290. if options["fields"] and field not in options["fields"]:
  2291. continue
  2292. if options["escape_data"]:
  2293. datum = escape(datum)
  2294. content = datum.replace("\n", linebreak)
  2295. lines.append(
  2296. f' <td style="'
  2297. f"padding-left: {lpad}em; "
  2298. f"padding-right: {rpad}em; "
  2299. f"text-align: {align}; "
  2300. f'vertical-align: {valign}">{content}</td>'
  2301. )
  2302. lines.append(" </tr>")
  2303. lines.append(" </tbody>")
  2304. lines.append("</table>")
  2305. return "\n".join(lines)
  2306. ##############################
  2307. # LATEX STRING METHODS #
  2308. ##############################
  2309. def get_latex_string(self, **kwargs) -> str:
  2310. """Return string representation of LaTex formatted version of table in current
  2311. state.
  2312. Arguments:
  2313. start - index of first data row to include in output
  2314. end - index of last data row to include in output PLUS ONE (list slice style)
  2315. fields - names of fields (columns) to include
  2316. header - print a header showing field names (True or False)
  2317. border - print a border around the table (True or False)
  2318. preserve_internal_border - print a border inside the table even if
  2319. border is disabled (True or False)
  2320. hrules - controls printing of horizontal rules after rows.
  2321. Allowed values: HRuleStyle
  2322. vrules - controls printing of vertical rules between columns.
  2323. Allowed values: VRuleStyle
  2324. int_format - controls formatting of integer data
  2325. float_format - controls formatting of floating point data
  2326. sortby - name of field to sort rows by
  2327. sort_key - sorting key function, applied to data points before sorting
  2328. row_filter - filter function applied on rows
  2329. format - Controls whether or not HTML tables are formatted to match
  2330. styling options (True or False)
  2331. """
  2332. options = self._get_options(kwargs)
  2333. if options["format"]:
  2334. string = self._get_formatted_latex_string(options)
  2335. else:
  2336. string = self._get_simple_latex_string(options)
  2337. return string
  2338. def _get_simple_latex_string(self, options: OptionsType) -> str:
  2339. lines: list[str] = []
  2340. wanted_fields = []
  2341. if options["fields"]:
  2342. wanted_fields = [
  2343. field for field in self._field_names if field in options["fields"]
  2344. ]
  2345. else:
  2346. wanted_fields = self._field_names
  2347. alignments = "".join([self._align[field] for field in wanted_fields])
  2348. begin_cmd = f"\\begin{{tabular}}{{{alignments}}}"
  2349. lines.append(begin_cmd)
  2350. # Headers
  2351. if options["header"]:
  2352. lines.append(" & ".join(wanted_fields) + " \\\\")
  2353. # Data
  2354. rows = self._get_rows(options)
  2355. formatted_rows = self._format_rows(rows)
  2356. for row in formatted_rows:
  2357. wanted_data = [
  2358. d for f, d in zip(self._field_names, row) if f in wanted_fields
  2359. ]
  2360. lines.append(" & ".join(wanted_data) + " \\\\")
  2361. lines.append("\\end{tabular}")
  2362. return "\r\n".join(lines)
  2363. def _get_formatted_latex_string(self, options: OptionsType) -> str:
  2364. lines: list[str] = []
  2365. if options["fields"]:
  2366. wanted_fields = [
  2367. field for field in self._field_names if field in options["fields"]
  2368. ]
  2369. else:
  2370. wanted_fields = self._field_names
  2371. wanted_alignments = [self._align[field] for field in wanted_fields]
  2372. if options["border"] and options["vrules"] == VRuleStyle.ALL:
  2373. alignment_str = "|".join(wanted_alignments)
  2374. elif not options["border"] and options["preserve_internal_border"]:
  2375. alignment_str = "|".join(wanted_alignments)
  2376. else:
  2377. alignment_str = "".join(wanted_alignments)
  2378. if options["border"] and options["vrules"] in [
  2379. VRuleStyle.ALL,
  2380. VRuleStyle.FRAME,
  2381. ]:
  2382. alignment_str = "|" + alignment_str + "|"
  2383. begin_cmd = f"\\begin{{tabular}}{{{alignment_str}}}"
  2384. lines.append(begin_cmd)
  2385. if options["border"] and options["hrules"] in [
  2386. HRuleStyle.ALL,
  2387. HRuleStyle.FRAME,
  2388. ]:
  2389. lines.append("\\hline")
  2390. # Headers
  2391. if options["header"]:
  2392. lines.append(" & ".join(wanted_fields) + " \\\\")
  2393. if (options["border"] or options["preserve_internal_border"]) and options[
  2394. "hrules"
  2395. ] in [HRuleStyle.ALL, HRuleStyle.HEADER]:
  2396. lines.append("\\hline")
  2397. # Data
  2398. rows = self._get_rows(options)
  2399. formatted_rows = self._format_rows(rows)
  2400. rows = self._get_rows(options)
  2401. for row in formatted_rows:
  2402. wanted_data = [
  2403. d for f, d in zip(self._field_names, row) if f in wanted_fields
  2404. ]
  2405. lines.append(" & ".join(wanted_data) + " \\\\")
  2406. if options["border"] and options["hrules"] == HRuleStyle.ALL:
  2407. lines.append("\\hline")
  2408. if options["border"] and options["hrules"] == HRuleStyle.FRAME:
  2409. lines.append("\\hline")
  2410. lines.append("\\end{tabular}")
  2411. return "\r\n".join(lines)
  2412. ##############################
  2413. # MEDIAWIKI STRING METHODS #
  2414. ##############################
  2415. def get_mediawiki_string(self, **kwargs) -> str:
  2416. """
  2417. Return string representation of the table in MediaWiki table markup.
  2418. The generated markup follows simple MediaWiki syntax. For example:
  2419. {| class="wikitable"
  2420. |+ Optional caption
  2421. |-
  2422. ! Header1 !! Header2 !! Header3
  2423. |-
  2424. | Data1 || Data2 || Data3
  2425. |-
  2426. | Data4 || Data5 || Data6
  2427. |}
  2428. """
  2429. options = self._get_options(kwargs)
  2430. lines: list[str] = []
  2431. if (
  2432. options.get("attributes")
  2433. and isinstance(options["attributes"], dict)
  2434. and options["attributes"]
  2435. ):
  2436. attr_str = " ".join(f'{k}="{v}"' for k, v in options["attributes"].items())
  2437. lines.append("{| " + attr_str)
  2438. else:
  2439. lines.append('{| class="wikitable"')
  2440. caption = options.get("title", self._title)
  2441. if caption:
  2442. lines.append("|+ " + caption)
  2443. if options.get("header"):
  2444. lines.append("|-")
  2445. headers = []
  2446. fields_option = options.get("fields")
  2447. for field in self._field_names:
  2448. if fields_option is not None and field not in fields_option:
  2449. continue
  2450. headers.append(field)
  2451. if headers:
  2452. header_line = " !! ".join(headers)
  2453. lines.append("! " + header_line)
  2454. rows = self._get_rows(options)
  2455. formatted_rows = self._format_rows(rows)
  2456. for row in formatted_rows:
  2457. lines.append("|-")
  2458. cells = []
  2459. fields_option = options.get("fields")
  2460. for field, cell in zip(self._field_names, row):
  2461. if fields_option is not None and field not in fields_option:
  2462. continue
  2463. cells.append(cell)
  2464. if cells:
  2465. lines.append("| " + " || ".join(cells))
  2466. lines.append("|}")
  2467. return "\n".join(lines)
  2468. ##############################
  2469. # UNICODE WIDTH FUNCTION #
  2470. ##############################
  2471. @lru_cache
  2472. def _str_block_width(val: str) -> int:
  2473. import wcwidth
  2474. val = _osc8_re.sub(r"\1", val)
  2475. return wcwidth.wcswidth(_re.sub("", val))
  2476. ##############################
  2477. # TABLE FACTORIES #
  2478. ##############################
  2479. def from_csv(fp, field_names: Sequence[str] | None = None, **kwargs) -> PrettyTable:
  2480. import csv
  2481. fmtparams = {}
  2482. for param in [
  2483. "delimiter",
  2484. "doublequote",
  2485. "escapechar",
  2486. "lineterminator",
  2487. "quotechar",
  2488. "quoting",
  2489. "skipinitialspace",
  2490. "strict",
  2491. ]:
  2492. if param in kwargs:
  2493. fmtparams[param] = kwargs.pop(param)
  2494. if fmtparams:
  2495. reader = csv.reader(fp, **fmtparams)
  2496. else:
  2497. dialect = csv.Sniffer().sniff(fp.read(1024))
  2498. fp.seek(0)
  2499. reader = csv.reader(fp, dialect)
  2500. table = PrettyTable(**kwargs)
  2501. if field_names:
  2502. table.field_names = field_names
  2503. else:
  2504. table.field_names = [x.strip() for x in next(reader)]
  2505. for row in reader:
  2506. table.add_row([x.strip() for x in row])
  2507. return table
  2508. def from_db_cursor(cursor: Cursor, **kwargs) -> PrettyTable | None:
  2509. if cursor.description:
  2510. table = PrettyTable(**kwargs)
  2511. table.field_names = [col[0] for col in cursor.description]
  2512. for row in cursor.fetchall():
  2513. table.add_row(row)
  2514. return table
  2515. return None
  2516. def from_json(json_string: str | bytes, **kwargs) -> PrettyTable:
  2517. import json
  2518. table = PrettyTable(**kwargs)
  2519. objects = json.loads(json_string)
  2520. table.field_names = objects[0]
  2521. for obj in objects[1:]:
  2522. row = [obj[key] for key in table.field_names]
  2523. table.add_row(row)
  2524. return table
  2525. class TableHandler(HTMLParser):
  2526. def __init__(self, **kwargs) -> None:
  2527. HTMLParser.__init__(self)
  2528. self.kwargs = kwargs
  2529. self.tables: list[PrettyTable] = []
  2530. self.last_row: list[str] = []
  2531. self.rows: list[tuple[list[str], bool]] = []
  2532. self.max_row_width = 0
  2533. self.active: str | None = None
  2534. self.last_content = ""
  2535. self.is_last_row_header = False
  2536. self.colspan = 0
  2537. def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
  2538. self.active = tag
  2539. if tag == "th":
  2540. self.is_last_row_header = True
  2541. for key, value in attrs:
  2542. if key == "colspan":
  2543. self.colspan = int(value) # type: ignore[arg-type]
  2544. def handle_endtag(self, tag: str) -> None:
  2545. if tag in ["th", "td"]:
  2546. stripped_content = self.last_content.strip()
  2547. self.last_row.append(stripped_content)
  2548. if self.colspan:
  2549. for _ in range(1, self.colspan):
  2550. self.last_row.append("")
  2551. self.colspan = 0
  2552. if tag == "tr":
  2553. self.rows.append((self.last_row, self.is_last_row_header))
  2554. self.max_row_width = max(self.max_row_width, len(self.last_row))
  2555. self.last_row = []
  2556. self.is_last_row_header = False
  2557. if tag == "table":
  2558. table = self.generate_table(self.rows)
  2559. self.tables.append(table)
  2560. self.rows = []
  2561. self.last_content = " "
  2562. self.active = None
  2563. def handle_data(self, data: str) -> None:
  2564. self.last_content += data
  2565. def generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
  2566. """
  2567. Generates from a list of rows a PrettyTable object.
  2568. """
  2569. table = PrettyTable(**self.kwargs)
  2570. for row in self.rows:
  2571. if len(row[0]) < self.max_row_width:
  2572. appends = self.max_row_width - len(row[0])
  2573. for i in range(1, appends):
  2574. row[0].append("-")
  2575. if row[1]:
  2576. self.make_fields_unique(row[0])
  2577. table.field_names = row[0]
  2578. else:
  2579. table.add_row(row[0])
  2580. return table
  2581. def make_fields_unique(self, fields: list[str]) -> None:
  2582. """
  2583. iterates over the row and make each field unique
  2584. """
  2585. for i in range(len(fields)):
  2586. for j in range(i + 1, len(fields)):
  2587. if fields[i] == fields[j]:
  2588. fields[j] += "'"
  2589. def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
  2590. """
  2591. Generates a list of PrettyTables from a string of HTML code. Each <table> in
  2592. the HTML becomes one PrettyTable object.
  2593. """
  2594. parser = TableHandler(**kwargs)
  2595. parser.feed(html_code)
  2596. return parser.tables
  2597. def from_html_one(html_code: str, **kwargs) -> PrettyTable:
  2598. """
  2599. Generates a PrettyTable from a string of HTML code which contains only a
  2600. single <table>
  2601. """
  2602. tables = from_html(html_code, **kwargs)
  2603. try:
  2604. assert len(tables) == 1
  2605. except AssertionError:
  2606. msg = "More than one <table> in provided HTML code. Use from_html instead."
  2607. raise ValueError(msg)
  2608. return tables[0]
  2609. def from_mediawiki(wiki_text: str, **kwargs) -> PrettyTable:
  2610. """
  2611. Returns a PrettyTable instance from simple MediaWiki table markup.
  2612. Note that the table should have a header row.
  2613. Arguments:
  2614. wiki_text -- Multiline string containing MediaWiki table markup
  2615. (Enter within ''' ''')
  2616. """
  2617. lines = wiki_text.strip().split("\n")
  2618. table = PrettyTable(**kwargs)
  2619. header = None
  2620. rows = []
  2621. inside_table = False
  2622. for line in lines:
  2623. line = line.strip()
  2624. if line.startswith("{|"):
  2625. inside_table = True
  2626. continue
  2627. if line.startswith("|}"):
  2628. break
  2629. if not inside_table:
  2630. continue
  2631. if line.startswith("|-"):
  2632. continue
  2633. if line.startswith("|+"):
  2634. continue
  2635. if line.startswith("!"):
  2636. header = [cell.strip() for cell in re.split(r"\s*!!\s*", line[1:])]
  2637. table.field_names = header
  2638. continue
  2639. if line.startswith("|"):
  2640. row_data = [cell.strip() for cell in re.split(r"\s*\|\|\s*", line[1:])]
  2641. rows.append(row_data)
  2642. continue
  2643. if header:
  2644. for row in rows:
  2645. if len(row) != len(header):
  2646. error_message = "Row length mismatch between header and body."
  2647. raise ValueError(error_message)
  2648. table.add_row(row)
  2649. else:
  2650. msg = "No valid header found in the MediaWiki table."
  2651. raise ValueError(msg)
  2652. return table
  2653. def _warn_deprecation(name: str, module_globals: dict[str, Any]) -> Any:
  2654. if (val := module_globals.get(f"_DEPRECATED_{name}")) is None:
  2655. msg = f"module '{__name__}' has no attribute '{name}'"
  2656. raise AttributeError(msg)
  2657. module_globals[name] = val
  2658. if name in {"FRAME", "ALL", "NONE", "HEADER"}:
  2659. msg = (
  2660. f"the '{name}' constant is deprecated, "
  2661. "use the 'HRuleStyle' and 'VRuleStyle' enums instead"
  2662. )
  2663. else:
  2664. msg = f"the '{name}' constant is deprecated, use the 'TableStyle' enum instead"
  2665. import warnings
  2666. warnings.warn(msg, DeprecationWarning, stacklevel=3)
  2667. return val
  2668. def __getattr__(name: str) -> Any:
  2669. return _warn_deprecation(name, module_globals=globals())