├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── poetry.lock ├── py_markdown_table ├── __init__.py ├── markdown_table.py └── utils.py ├── pyproject.toml ├── res └── table_w_emoji.jpg ├── tests └── test_markdown_table.py └── utils ├── benchmark.py └── gendesc.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | day: saturday 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | test: 12 | name: Test with different Python versions 13 | runs-on: ubuntu-22.04 14 | strategy: 15 | matrix: 16 | python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install pipx 27 | run: | 28 | sudo apt update 29 | sudo apt install pipx 30 | pipx ensurepath 31 | sudo pipx ensurepath --global 32 | 33 | - name: Install Poetry 34 | run: | 35 | pipx install poetry 36 | 37 | - name: Install dependencies 38 | run: | 39 | poetry install 40 | 41 | - name: Test with ruff 42 | run: | 43 | poetry run ruff check . 44 | 45 | - name: Test with pytest 46 | run: | 47 | poetry run pytest . 48 | 49 | 50 | build-and-publish: 51 | name: Build and Publish 52 | runs-on: ubuntu-22.04 53 | needs: test 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v4 57 | 58 | - name: Set up Python 3.9 59 | uses: actions/setup-python@v5 60 | with: 61 | python-version: 3.9 62 | 63 | - name: Install pipx 64 | run: | 65 | sudo apt update 66 | sudo apt install pipx 67 | pipx ensurepath 68 | sudo pipx ensurepath --global 69 | 70 | - name: Install Poetry 71 | run: | 72 | pipx install poetry 73 | 74 | - name: Install dependencies 75 | run: | 76 | poetry install 77 | 78 | - name: Build and publish to pypi 79 | uses: JRubics/poetry-publish@v2.1 80 | with: 81 | pypi_token: ${{ secrets.PYPI_PASSWORD }} 82 | 83 | - name: Test with pytest and coverage 84 | run: | 85 | poetry run pytest --cov=./py_markdown_table/ --cov-report=xml 86 | 87 | - name: Upload coverage to Codecov 88 | uses: codecov/codecov-action@v5 89 | env: 90 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 91 | with: 92 | fail_ci_if_error: false -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'README.md' 6 | branches: 7 | - '**' 8 | pull_request: 9 | paths-ignore: 10 | - 'README.md' 11 | branches: 12 | - '**' 13 | 14 | jobs: 15 | test: 16 | name: Test with different Python versions 17 | runs-on: ubuntu-22.04 18 | strategy: 19 | matrix: 20 | python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install pipx 31 | run: | 32 | sudo apt update 33 | sudo apt install pipx 34 | pipx ensurepath 35 | sudo pipx ensurepath --global 36 | 37 | - name: Install Poetry 38 | run: | 39 | pipx install poetry 40 | 41 | - name: Install dependencies 42 | run: | 43 | poetry install 44 | 45 | - name: Test with ruff 46 | run: | 47 | poetry run ruff check . 48 | 49 | - name: Test with pytest 50 | run: | 51 | poetry run pytest . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pytest cache 2 | .pytest_cache 3 | py_markdown_table.egg-info 4 | __pycache__ 5 | build 6 | debug.log 7 | .vscode 8 | *coverage* 9 | .ruff_cache 10 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hvalev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-markdown-table 2 | [![build](https://github.com/hvalev/py-markdown-table/actions/workflows/build.yml/badge.svg)](https://github.com/hvalev/py-markdown-table/actions/workflows/build.yml) 3 | [![codecov](https://codecov.io/gh/hvalev/py-markdown-table/branch/main/graph/badge.svg?token=ZZ8WXO4H6P)](https://codecov.io/gh/hvalev/py-markdown-table) 4 | [![Downloads](https://static.pepy.tech/badge/py-markdown-table)](https://pepy.tech/project/py-markdown-table) 5 | [![Downloads](https://static.pepy.tech/badge/py-markdown-table/month)](https://pepy.tech/project/py-markdown-table) 6 | [![Downloads](https://static.pepy.tech/badge/py-markdown-table/week)](https://pepy.tech/project/py-markdown-table) 7 | 8 | Tiny python library with zero dependencies which generates formatted multiline tables in `markdown`. 9 | 10 | ## Basic Use 11 | Install via pip as follows: 12 | ```bash 13 | pip install py-markdown-table 14 | ``` 15 | 16 | Pass a `list` of `dict`s where the `dict`s must have uniform keys which serve as column headers and the values are expanded to be rows. Simple example with no special formatting: 17 | ```python 18 | from py_markdown_table.markdown_table import markdown_table 19 | data = [ 20 | { 21 | "Product": "Smartphone", 22 | "Brand": "Apple", 23 | "Price": 999.99 24 | }, 25 | { 26 | "Product": "Laptop", 27 | "Brand": "Dell", 28 | "Price": 1299.99 29 | } 30 | ] 31 | markdown = markdown_table(data).get_markdown() 32 | print(markdown) 33 | ``` 34 | 35 | ``` 36 | +------------------------+ 37 | | Product |Brand| Price | 38 | +----------+-----+-------+ 39 | |Smartphone|Apple| 999.99| 40 | +----------+-----+-------+ 41 | | Laptop | Dell|1299.99| 42 | +------------------------+ 43 | ``` 44 | 45 | A more comprehensive example showcasing some of the formatting options: 46 | ```python 47 | from py_markdown_table.markdown_table import markdown_table 48 | jokes_list = [ 49 | { 50 | "joke1": "Why don't scientists trust atoms? Because they make up everything!", 51 | "joke2": "Did you hear about the mathematician who's afraid of negative numbers? He will stop at nothing to avoid them!", 52 | "joke3": "Why don't skeletons fight each other? They don't have the guts!" 53 | }, 54 | { 55 | "joke1": "What do you call a snowman with a six-pack? An abdominal snowman!", 56 | "joke2": "Why don't eggs tell jokes? Because they might crack up!", 57 | "joke3": "How does a penguin build its house? Igloos it together!" 58 | } 59 | ] 60 | markdown = markdown_table(jokes_list).set_params(padding_width = 3, 61 | padding_weight = 'centerleft', 62 | multiline = {'joke1': 30, 'joke2': 30, 'joke3': 30} 63 | ).get_markdown() 64 | ``` 65 | ``` 66 | +--------------------------------------------------------------------------------------------------------------+ 67 | | joke1 | joke2 | joke3 | 68 | +------------------------------------+------------------------------------+------------------------------------+ 69 | | Why don't scientists trust atoms? | Did you hear about the | Why don't skeletons fight each | 70 | | Because they make up everything! | mathematician who's afraid of | other? They don't have the guts! | 71 | | | negative numbers? He will stop at | | 72 | | | nothing to avoid them! | | 73 | +------------------------------------+------------------------------------+------------------------------------+ 74 | | What do you call a snowman with a | Why don't eggs tell jokes? | How does a penguin build its | 75 | | six-pack? An abdominal snowman! | Because they might crack up! | house? Igloos it together! | 76 | +--------------------------------------------------------------------------------------------------------------+ 77 | ``` 78 | 79 | You can also use pandas dataframes by formatting them as follows: 80 | ```python 81 | from py_markdown_table.markdown_table import markdown_table 82 | data = df.to_dict(orient='records') 83 | markdown_table(data).get_markdown() 84 | ``` 85 | 86 | ## Advanced Use 87 | To add parameters to how the markdown table is formatted, you can use the `set_params()` function on a `markdown_table` object, i.e. `markdown_table(data).set_params(...).get_markdown()`, which allows you to pass the following keyword arguments: 88 | 89 | ``` 90 | +--------------------------------------------------------------------------------------------------+ 91 | | param | type | values | description | 92 | +-----------------------+---------------------+-------------------+--------------------------------+ 93 | | row_sep | str | | Row separation strategy using | 94 | | | | | `----` as pattern | 95 | +-----------------------+---------------------+-------------------+--------------------------------+ 96 | | | | always | Separate each row | 97 | +-----------------------+---------------------+-------------------+--------------------------------+ 98 | | | | topbottom | Separate the top (header) and | 99 | | | | | bottom (last row) of the table | 100 | +-----------------------+---------------------+-------------------+--------------------------------+ 101 | | | | markdown | Separate only header from body | 102 | +-----------------------+---------------------+-------------------+--------------------------------+ 103 | | | | None | No row separators will be | 104 | | | | | inserted | 105 | +-----------------------+---------------------+-------------------+--------------------------------+ 106 | | padding_width | int or | | Allocate padding to all table | 107 | | | dict | | cells when passing an int or | 108 | | | | | per-column when passing a dict | 109 | +-----------------------+---------------------+-------------------+--------------------------------+ 110 | | padding_weight | str or | | Strategy for allocating | 111 | | | dict | | padding within table cells. | 112 | | | | | Per-column when passing a dict | 113 | +-----------------------+---------------------+-------------------+--------------------------------+ 114 | | | | left | Aligns the cell's contents to | 115 | | | | | the end of the cell | 116 | +-----------------------+---------------------+-------------------+--------------------------------+ 117 | | | | right | Aligns the cell's contents to | 118 | | | | | the beginning of the cell | 119 | +-----------------------+---------------------+-------------------+--------------------------------+ 120 | | | | centerleft | Centers cell's contents with | 121 | | | | | extra padding allocated to the | 122 | | | | | beginning of the cell | 123 | +-----------------------+---------------------+-------------------+--------------------------------+ 124 | | | | centerright | Centers cell's contents with | 125 | | | | | extra padding allocated to the | 126 | | | | | end of the cell | 127 | +-----------------------+---------------------+-------------------+--------------------------------+ 128 | | padding_char | str | | Single character used to fill | 129 | | | | | padding with. Default is a | 130 | | | | | blank space ` `. | 131 | +-----------------------+---------------------+-------------------+--------------------------------+ 132 | | newline_char | str | | Character appended to each row | 133 | | | | | to force a newline. Default is | 134 | | | | | `\n` | 135 | +-----------------------+---------------------+-------------------+--------------------------------+ 136 | | float_rounding | int | | Integer denoting the precision | 137 | | | | | of cells of `floats` after the | 138 | | | | | decimal point. Default is | 139 | | | | | `None`. | 140 | +-----------------------+---------------------+-------------------+--------------------------------+ 141 | | emoji_spacing | str | | Strategy for rendering emojis | 142 | | | | | in tables. Currently only | 143 | | | | | `mono` is supported for | 144 | | | | | monospaced fonts. Default is | 145 | | | | | `None` which disables special | 146 | | | | | handling of emojis. | 147 | +-----------------------+---------------------+-------------------+--------------------------------+ 148 | | multiline | dict | | Renders the table with | 149 | | | | | predefined widths by passing a | 150 | | | | | `dict` with `keys` being the | 151 | | | | | column names (e.g. equivalent | 152 | | | | | to those in the passed `data` | 153 | | | | | variable) and `values` -- the | 154 | | | | | `width` of each column as an | 155 | | | | | integer. Note that the width | 156 | | | | | of a column cannot be smaller | 157 | | | | | than the longest contiguous | 158 | | | | | string present in the data. | 159 | +-----------------------+---------------------+-------------------+--------------------------------+ 160 | | multiline_strategy | str | | Strategy applied to rendering | 161 | | | | | contents in multiple lines. | 162 | | | | | Possible values are `rows`, | 163 | | | | | `header` or `rows_and_header`. | 164 | | | | | The default value is `rows`. | 165 | +-----------------------+---------------------+-------------------+--------------------------------+ 166 | | | | rows | Splits only rows overfilling | 167 | | | | | by the predefined column width | 168 | | | | | as provided in the `multiline` | 169 | | | | | variable | 170 | +-----------------------+---------------------+-------------------+--------------------------------+ 171 | | | | header | Splits only the header | 172 | | | | | overfilling by the predefined | 173 | | | | | column width as provided in | 174 | | | | | the `multiline` variable | 175 | +-----------------------+---------------------+-------------------+--------------------------------+ 176 | | | | rows_and_header | Splits rows and header | 177 | | | | | overfilling by the predefined | 178 | | | | | column width as provided in | 179 | | | | | the `multiline` variable | 180 | +-----------------------+---------------------+-------------------+--------------------------------+ 181 | | multiline_delimiter | str | | Character that will be used to | 182 | | | | | split a cell's contents into | 183 | | | | | multiple rows. The default | 184 | | | | | value is a blank space ` `. | 185 | +-----------------------+---------------------+-------------------+--------------------------------+ 186 | | quote | bool | | Wraps the generated markdown | 187 | | | | | table in block quotes | 188 | | | | | ```table```. Default is | 189 | | | | | `True`. | 190 | +--------------------------------------------------------------------------------------------------+ 191 | ``` 192 | ## Utils 193 | The namespace `py_markdown_table.utils` provides the functions `count_emojis()` and `find_longest_contiguous_strings()`. `count_emojis()` detects emojis and their position in a given string, and `find_longest_contiguous_strings()` finds the longest continuous strings present in the rows and/or columns of your input data. `find_longest_contiguous_strings()` can be useful to figure out the minimal width of each column given a particular data. 194 | 195 | ## Further Examples 196 | ### Row separatation 197 | ```python 198 | markdown_table(data).set_params(row_sep = 'always').get_markdown() 199 | ``` 200 |
201 | 202 | see example 203 | 204 | 205 | ``` 206 | +----------------------------------------+ 207 | | title | time | date |seats| 208 | +------------+-----------+---------+-----+ 209 | |Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24| 210 | +------------+-----------+---------+-----+ 211 | |Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18| 212 | +------------+-----------+---------+-----+ 213 | |Vrij zwemmen| 7:30-8:30 |Fri 11.12|18/18| 214 | +------------+-----------+---------+-----+ 215 | |Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18| 216 | +----------------------------------------+ 217 | ``` 218 |
219 |
220 | 221 | ```python 222 | markdown_table(data).set_params(row_sep = 'topbottom').get_markdown() 223 | ``` 224 |
225 | 226 | see example 227 | 228 | 229 | ``` 230 | +----------------------------------------+ 231 | | title | time | date |seats| 232 | |Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24| 233 | |Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18| 234 | |Vrij zwemmen| 7:30-8:30 |Fri 11.12|18/18| 235 | |Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18| 236 | +----------------------------------------+ 237 | ``` 238 |
239 |
240 | 241 | ```python 242 | markdown_table(data).set_params(row_sep = 'markdown').get_markdown() 243 | ``` 244 |
245 | 246 | see example 247 | 248 | 249 | ``` 250 | | title | time | date |seats| 251 | |------------|-----------|---------|-----| 252 | |Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24| 253 | |Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18| 254 | |Vrij zwemmen| 7:30-8:30 |Fri 11.12|18/18| 255 | |Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18| 256 | ``` 257 |
258 |
259 | 260 | 261 | ```python 262 | markdown_table(data).set_params(row_sep = 'markdown', quote = False).get_markdown() 263 | ``` 264 |
265 | 266 | see example 267 | 268 | 269 | | title | time | date |seats| 270 | |------------|-----------|---------|-----| 271 | |Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24| 272 | |Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18| 273 | |Vrij zwemmen| 7:30-8:30 |Fri 11.12|18/18| 274 | |Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18| 275 |
276 |
277 | 278 | 279 | ### Padding, padding weight and padding char 280 | ```python 281 | markdown_table(data).set_params(row_sep = 'topbottom', padding_width = 5, padding_weight = 'left').get_markdown() 282 | ``` 283 |
284 | 285 | see example 286 | 287 | 288 | ``` 289 | +------------------------------------------------------------+ 290 | | title| time| date| seats| 291 | | Vrij Zwemmen| 21:30-23:00| Wed 09.12| 24/24| 292 | | Vrij Zwemmen| 12:00-13:00| Thu 10.12| 18/18| 293 | | Vrij zwemmen| 7:30-8:30| Fri 11.12| 18/18| 294 | | Vrij Zwemmen| 13:15-14:15| Sat 12.12| 18/18| 295 | +------------------------------------------------------------+ 296 | ``` 297 |
298 |
299 | 300 | 301 | ```python 302 | markdown_table(data).set_params(row_sep = 'topbottom', padding_width = 5, padding_weight = 'centerright').get_markdown() 303 | ``` 304 |
305 | 306 | see example 307 | 308 | 309 | ``` 310 | +------------------------------------------------------------+ 311 | | title | time | date | seats | 312 | | Vrij Zwemmen | 21:30-23:00 | Wed 09.12 | 24/24 | 313 | | Vrij Zwemmen | 12:00-13:00 | Thu 10.12 | 18/18 | 314 | | Vrij zwemmen | 7:30-8:30 | Fri 11.12 | 18/18 | 315 | | Vrij Zwemmen | 13:15-14:15 | Sat 12.12 | 18/18 | 316 | +------------------------------------------------------------+ 317 | ``` 318 |
319 |
320 | 321 | 322 | ```python 323 | markdown_table(data).set_params(row_sep = 'always', padding_width = 5, padding_weight = 'centerright', padding_char = '.').get_markdown() 324 | ``` 325 |
326 | 327 | see example 328 | 329 | 330 | ``` 331 | +------------------------------------------------------------+ 332 | |......title......|......time......|.....date.....|..seats...| 333 | +-----------------+----------------+--------------+----------+ 334 | |..Vrij Zwemmen...|..21:30-23:00...|..Wed 09.12...|..24/24...| 335 | +-----------------+----------------+--------------+----------+ 336 | |..Vrij Zwemmen...|..12:00-13:00...|..Thu 10.12...|..18/18...| 337 | +-----------------+----------------+--------------+----------+ 338 | |..Vrij zwemmen...|...7:30-8:30....|..Fri 11.12...|..18/18...| 339 | +-----------------+----------------+--------------+----------+ 340 | |..Vrij Zwemmen...|..13:15-14:15...|..Sat 12.12...|..18/18...| 341 | +------------------------------------------------------------+ 342 | ``` 343 |
344 |
345 | 346 | ```python 347 | markdown_table(data).set_params(row_sep = 'always', padding_width = 5, padding_weight = 'centerright', padding_char = '.').get_markdown() 348 | ``` 349 |
350 | 351 | see example 352 | 353 | 354 | ``` 355 | +------------------------------------------------------------+ 356 | |......title......|......time......|.....date.....|..seats...| 357 | +-----------------+----------------+--------------+----------+ 358 | |..Vrij Zwemmen...|..21:30-23:00...|..Wed 09.12...|..24/24...| 359 | +-----------------+----------------+--------------+----------+ 360 | |..Vrij Zwemmen...|..12:00-13:00...|..Thu 10.12...|..18/18...| 361 | +-----------------+----------------+--------------+----------+ 362 | |..Vrij zwemmen...|...7:30-8:30....|..Fri 11.12...|..18/18...| 363 | +-----------------+----------------+--------------+----------+ 364 | |..Vrij Zwemmen...|..13:15-14:15...|..Sat 12.12...|..18/18...| 365 | +------------------------------------------------------------+ 366 | ``` 367 |
368 |
369 | 370 | ### Per-column padding and padding weight 371 | ```python 372 | markdown_table(data).set_params(row_sep = 'always', padding_width = {"title": 2, "time": 4, "date": 3, "seats": 1}, padding_weight = {"title": "left", "time": "right", "date": "centerleft", "seats": "centerright"}).get_markdown() 373 | 374 | ``` 375 |
376 | 377 | see example 378 | 379 | 380 | ``` 381 | +--------------------------------------------------+ 382 | | title|time | date |seats | 383 | +--------------+---------------+------------+------+ 384 | | Vrij Zwemmen|21:30-23:00 | Wed 09.12 |24/24 | 385 | +--------------+---------------+------------+------+ 386 | | Vrij Zwemmen|12:00-13:00 | Thu 10.12 |18/18 | 387 | +--------------+---------------+------------+------+ 388 | | Vrij Zwemmen|7:30-8:30 | Fri 11.12 |18/18 | 389 | +--------------+---------------+------------+------+ 390 | | Vrij Zwemmen|13:15-14:15 | Sat 12.12 |18/18 | 391 | +--------------------------------------------------+ 392 | ``` 393 |
394 |
395 | 396 | 397 | ```python 398 | markdown_table(data).set_params(row_sep = 'always', padding_width = {"A": 2, "B": 4, "C": 3}, padding_weight = {"A": "left", "B": "right", "C": "centerleft"}).get_markdown() 399 | ``` 400 |
401 | 402 | see example 403 | 404 | 405 | ``` 406 | +-----------------------------------------------------------------------+ 407 | | A|B | C | 408 | +-----------------------------+-------------------------------+---------+ 409 | | row1_A and additional stuff|row1_B | row1_C | 410 | +-----------------------------+-------------------------------+---------+ 411 | | row2_A|row2_B and additional stuff | row2_C | 412 | +-----------------------------+-------------------------------+---------+ 413 | | row3_A|row3_B | row3_C | 414 | +-----------------------------------------------------------------------+ 415 | ``` 416 |
417 |
418 | 419 | ### Multiline and emoji 420 | ```python 421 | markdown_table(data).set_params(padding_width = 0, padding_weight = "centerleft", multiline = {"A": 25, "B": 12, "C": 9}).get_markdown() 422 | ``` 423 |
424 | 425 | see example 426 | 427 | 428 | ``` 429 | +------------------------------------------------+ 430 | | A | B | C | 431 | +-------------------------+------------+---------+ 432 | | row1_A and additional | row1_B | row1_C | 433 | | stuff | | | 434 | +-------------------------+------------+---------+ 435 | | row2_A | row2_B and | row2_C | 436 | | | additional | | 437 | | | stuff | | 438 | +-------------------------+------------+---------+ 439 | | row3_A | row3_B | row3_C | 440 | +------------------------------------------------+ 441 | ``` 442 |
443 |
444 | 445 | 446 | ```python 447 | markdown_table(data).set_params(padding_width = 2, padding_weight = "centerleft", multiline = {"A": 25, "B": 12, "C": 9}).get_markdown()) 448 | ``` 449 |
450 | 451 | see example 452 | 453 | 454 | ``` 455 | +------------------------------------------------------------+ 456 | | A | B | C | 457 | +-----------------------------+----------------+-------------+ 458 | | row1_A and additional stuff | row1_B | row1_C | 459 | +-----------------------------+----------------+-------------+ 460 | | row2_A | row2_B and | row2_C | 461 | | | additional | | 462 | | | stuff | | 463 | +-----------------------------+----------------+-------------+ 464 | | row3_A | row3_B | row3_C | 465 | +------------------------------------------------------------+ 466 | ``` 467 |
468 |
469 | 470 | 471 | ```python 472 | markdown_table(data).set_params(row_sep = "always", multiline = {"those are multi rows": 5}, multiline_strategy = "rows_and_header").get_markdown() 473 | ``` 474 |
475 | 476 | see example 477 | 478 | 479 | ``` 480 | +-----+ 481 | |those| 482 | | are | 483 | |multi| 484 | | rows| 485 | +-----+ 486 | | yes | 487 | | they| 488 | | are | 489 | +-----+ 490 | | no | 491 | | they| 492 | | are | 493 | | not | 494 | +-----+ 495 | ``` 496 |
497 |
498 | 499 | 500 | ```python 501 | markdown_table(data).set_params(row_sep = "topbottom", emoji_spacing = "mono", multiline = {"title that is maybe too long": 7, "time": 11, "date": 5, "seats": 5,}, multiline_strategy = "rows_and_header").get_markdown() 502 | ``` 503 |
504 | 505 | see example 506 | 507 | *Note:* Github's markdown preview does not render emojis as two whole characters, hence the slight offsets in cells containing emojis. 508 | 509 | ``` 510 | +-------------------------------+ 511 | | title | time | date|seats| 512 | |that is| | | | 513 | | maybe | | | | 514 | | too | | | | 515 | | long | | | | 516 | | Vrij |21:30-23:00| 😊 |24/24| 517 | |Zwemmen| | | | 518 | | Vrij |12:00-13:00| Thu |18/18| 519 | |Zwemmen| |10.12| | 520 | | Vrij | 7:30-8:30 | Fri | 😊 | 521 | |Zwemmen| |11.12|🌍 🎉| 522 | | Vrij |13:15-14:15| Sat |20/20| 523 | |Zwemmen| |12.12| | 524 | | Vrij | 7:30-8:30 | Fri | asd | 525 | |Zwemmen| |11.12| 😊-| 526 | | | | | 🌍: | 527 | | | | | 🎉 | 528 | |Zwemmen|13:15-14:15| Sat |20/20| 529 | | | |12.12| | 530 | +-------------------------------+ 531 | ``` 532 | 533 | Below is an example from a monospaced terminal, where the table is rendered correctly. 534 | 535 | ![Table with emoji in terminal](res/table_w_emoji.jpg) 536 |
537 | 538 | 539 | ## Benchmarks 540 | The table below provide some benchmark results, evaluating the performance on data containing incrementally larger number of `columns`, `rows`, and characters in each table cell (i.e. `cell_size`). You can benchmark it on your own system using the script contained within `py_markdown_table/utils/benchmark.py`. Generally, reasonably-sized tables intended to be read by a human can be generated within a millisecond. 541 | 542 |
543 | 544 | see benchmark 545 | 546 | 547 | ``` 548 | +-----------------------------------------------+ 549 | | parameters |Multiline| speed | 550 | +------------------+---------+------------------+ 551 | | columns: 2 | False | 0.000000 ms | 552 | | rows: 10 | | | 553 | | cell_size: 5 | | | 554 | +------------------+---------+------------------+ 555 | | columns: 4 | False | 0.000000 ms | 556 | | rows: 40 | | | 557 | | cell_size: 20 | | | 558 | +------------------+---------+------------------+ 559 | | columns: 8 | False | 6.999756 ms | 560 | | rows: 160 | | | 561 | | cell_size: 80 | | | 562 | +------------------+---------+------------------+ 563 | | columns: 16 | False | 1173.794678 ms | 564 | | rows: 640 | | | 565 | | cell_size: 320 | | | 566 | +------------------+---------+------------------+ 567 | | columns: 2 | True | 0.000000 ms | 568 | | rows: 10 | | | 569 | | cell_size: 5 | | | 570 | +------------------+---------+------------------+ 571 | | columns: 4 | True | 0.996338 ms | 572 | | rows: 40 | | | 573 | | cell_size: 20 | | | 574 | +------------------+---------+------------------+ 575 | | columns: 8 | True | 16.038330 ms | 576 | | rows: 160 | | | 577 | | cell_size: 80 | | | 578 | +------------------+---------+------------------+ 579 | | columns: 16 | True | 1448.473633 ms | 580 | | rows: 640 | | | 581 | | cell_size: 320 | | | 582 | +-----------------------------------------------+ 583 | ``` 584 |
585 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "colorama" 5 | version = "0.4.6" 6 | description = "Cross-platform colored terminal text." 7 | optional = false 8 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 9 | files = [ 10 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 11 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 12 | ] 13 | 14 | [[package]] 15 | name = "coverage" 16 | version = "7.6.1" 17 | description = "Code coverage measurement for Python" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, 22 | {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, 23 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, 24 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, 25 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, 26 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, 27 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, 28 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, 29 | {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, 30 | {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, 31 | {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, 32 | {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, 33 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, 34 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, 35 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, 36 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, 37 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, 38 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, 39 | {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, 40 | {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, 41 | {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, 42 | {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, 43 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, 44 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, 45 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, 46 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, 47 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, 48 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, 49 | {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, 50 | {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, 51 | {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, 52 | {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, 53 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, 54 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, 55 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, 56 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, 57 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, 58 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, 59 | {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, 60 | {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, 61 | {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, 62 | {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, 63 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, 64 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, 65 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, 66 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, 67 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, 68 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, 69 | {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, 70 | {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, 71 | {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, 72 | {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, 73 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, 74 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, 75 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, 76 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, 77 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, 78 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, 79 | {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, 80 | {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, 81 | {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, 82 | {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, 83 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, 84 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, 85 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, 86 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, 87 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, 88 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, 89 | {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, 90 | {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, 91 | {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, 92 | {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, 93 | ] 94 | 95 | [package.dependencies] 96 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 97 | 98 | [package.extras] 99 | toml = ["tomli"] 100 | 101 | [[package]] 102 | name = "exceptiongroup" 103 | version = "1.2.2" 104 | description = "Backport of PEP 654 (exception groups)" 105 | optional = false 106 | python-versions = ">=3.7" 107 | files = [ 108 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 109 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 110 | ] 111 | 112 | [package.extras] 113 | test = ["pytest (>=6)"] 114 | 115 | [[package]] 116 | name = "iniconfig" 117 | version = "2.0.0" 118 | description = "brain-dead simple config-ini parsing" 119 | optional = false 120 | python-versions = ">=3.7" 121 | files = [ 122 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 123 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 124 | ] 125 | 126 | [[package]] 127 | name = "packaging" 128 | version = "24.2" 129 | description = "Core utilities for Python packages" 130 | optional = false 131 | python-versions = ">=3.8" 132 | files = [ 133 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 134 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 135 | ] 136 | 137 | [[package]] 138 | name = "pluggy" 139 | version = "1.5.0" 140 | description = "plugin and hook calling mechanisms for python" 141 | optional = false 142 | python-versions = ">=3.8" 143 | files = [ 144 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 145 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 146 | ] 147 | 148 | [package.extras] 149 | dev = ["pre-commit", "tox"] 150 | testing = ["pytest", "pytest-benchmark"] 151 | 152 | [[package]] 153 | name = "pytest" 154 | version = "8.3.4" 155 | description = "pytest: simple powerful testing with Python" 156 | optional = false 157 | python-versions = ">=3.8" 158 | files = [ 159 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 160 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 161 | ] 162 | 163 | [package.dependencies] 164 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 165 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 166 | iniconfig = "*" 167 | packaging = "*" 168 | pluggy = ">=1.5,<2" 169 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 170 | 171 | [package.extras] 172 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 173 | 174 | [[package]] 175 | name = "pytest-cov" 176 | version = "5.0.0" 177 | description = "Pytest plugin for measuring coverage." 178 | optional = false 179 | python-versions = ">=3.8" 180 | files = [ 181 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 182 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 183 | ] 184 | 185 | [package.dependencies] 186 | coverage = {version = ">=5.2.1", extras = ["toml"]} 187 | pytest = ">=4.6" 188 | 189 | [package.extras] 190 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 191 | 192 | [[package]] 193 | name = "ruff" 194 | version = "0.9.4" 195 | description = "An extremely fast Python linter and code formatter, written in Rust." 196 | optional = false 197 | python-versions = ">=3.7" 198 | files = [ 199 | {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, 200 | {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, 201 | {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, 202 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, 203 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, 204 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, 205 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, 206 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, 207 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, 208 | {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, 209 | {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, 210 | {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, 211 | {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, 212 | {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, 213 | {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, 214 | {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, 215 | {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, 216 | {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, 217 | ] 218 | 219 | [[package]] 220 | name = "tomli" 221 | version = "2.2.1" 222 | description = "A lil' TOML parser" 223 | optional = false 224 | python-versions = ">=3.8" 225 | files = [ 226 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 227 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 228 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 229 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 230 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 231 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 232 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 233 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 234 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 235 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 236 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 237 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 238 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 239 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 240 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 241 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 242 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 243 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 244 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 245 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 246 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 247 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 248 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 249 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 250 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 251 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 252 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 253 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 254 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 255 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 256 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 257 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 258 | ] 259 | 260 | [metadata] 261 | lock-version = "2.0" 262 | python-versions = "^3.8" 263 | content-hash = "77613138678ec05e97ebd12e478b9e431860e267617946196201fc20d062c7e8" 264 | -------------------------------------------------------------------------------- /py_markdown_table/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hvalev/py-markdown-table/3e464cb79abbe1063745fa4f71ae65371ab36bbd/py_markdown_table/__init__.py -------------------------------------------------------------------------------- /py_markdown_table/markdown_table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Class used to generate formatted markdown tables. See class description""" 3 | import math 4 | from typing import Optional, List, Dict, Union 5 | from py_markdown_table.utils import count_emojis, split_list_by_indices 6 | 7 | 8 | class markdown_table: # noqa: N801 9 | """ 10 | Class used to generate padded tables in a markdown code block 11 | 12 | Methods 13 | ------- 14 | get_markdown() 15 | gets complete escaped markdown table 16 | get_header() 17 | gets unescaped table header 18 | get_body() 19 | gets unescaped table content 20 | """ 21 | 22 | def __init__( 23 | self, 24 | data: Union[List[Dict], Dict], 25 | skip_data_validation: bool = False, 26 | ): 27 | """ 28 | Initialize markdown_table with support for various rendering parameters. 29 | 30 | Args: 31 | `data` (List[Dict]): The data to be rendered in the markdown table. \n 32 | `skip_data_validation` (bool, optional): skip the data validation step before rending a table. Useful when renderers change the length of a string (i.e. markdown urls). \n 33 | Default is `False` \n 34 | 35 | """ 36 | if not isinstance(data, list) or not all(isinstance(elem, dict) for elem in data): 37 | raise ValueError("data is not of type list or elements are not of type dict") 38 | if len(data) == 0: 39 | raise ValueError("Data variable contains no elements.") 40 | self.data = data 41 | 42 | # set defaults 43 | self.row_sep = "always" 44 | self.padding_width = {key: 0 for key in self.data[0].keys()} 45 | self.padding_weight = {key: "centerleft" for key in self.data[0].keys()} 46 | self.padding_char = " " 47 | self.newline_char = "\n" 48 | self.float_rounding = None 49 | self.emoji_spacing = None 50 | self.multiline = None 51 | self.multiline_strategy = "rows" 52 | self.multiline_delimiter = " " 53 | self.quote = True 54 | self.skip_data_validation = skip_data_validation 55 | 56 | self.__validate_parameters() 57 | 58 | if not self.skip_data_validation: 59 | self.__validate_data(data) 60 | 61 | self.__update_meta_params() 62 | 63 | # we need to first update the meta_params for cell width, padding etc 64 | # prior to checking whether the data will fit for multiline rendering 65 | if self.multiline: 66 | self.__validate_multiline(self.data) 67 | 68 | 69 | def set_params( 70 | self, 71 | row_sep: str = "always", 72 | padding_width: Union[int, Dict[str,int]] = 0, 73 | padding_weight: Union[str, Dict[str,str]] = "centerleft", 74 | padding_char: str = " ", 75 | newline_char: str = "\n", 76 | float_rounding: Optional[int] = None, 77 | emoji_spacing: Optional[str] = None, 78 | multiline: Optional[Dict] = None, 79 | multiline_strategy: str = "rows", 80 | multiline_delimiter: str = " ", 81 | quote: bool = True, 82 | ): 83 | """ 84 | Setter function for markdown table rendering parameters. 85 | 86 | Args: 87 | `row_sep` (str, optional): Row separation strategy using `----` as pattern. Possible values are: 88 | `always`: Separate each row. 89 | `topbottom`: Separate the top (header) and bottom (last row) of the table. 90 | `markdown`: Separate only the header from the body. 91 | `None`: No row separators will be inserted. Defaults to "always". \n 92 | `padding_width` (Union[int, Dict[str, int]], optional): Width of padding to allocate to all table cells. Defaults to `0`. Padding can also be assigned on a per-column level by passing the values as key-value pairs where the keys mirror the ones of the input data and values describe the padding for the specific column.\n 93 | `padding_weight` (Union[str, Dict[str, str]], optional): Strategy for allocating padding within table cells. Padding weight can be assigned globally for the whole table as a string or per-column by passing the values as key-value pairs where the keys mirror the ones of the input data and values describe the padding weight for the specific column. Possible values are: 94 | `left`: Aligns the cell's contents to the end of the cell. 95 | `right`: Aligns the cell's contents to the beginning of the cell. 96 | `centerleft`: Centers cell's contents with extra padding allocated to the beginning of the cell. 97 | `centerright`: Centers cell's contents with extra padding allocated to the end of the cell. 98 | Defaults to `centerleft`. \n 99 | `padding_char` (str, optional): Single character used to fill padding. Default is a blank space ` `. \n 100 | `newline_char` (str, optional): Character appended to each row to force a newline. Default is `\\n`. \n 101 | `float_rounding` (int, optional): Integer denoting the precision of cells with `float` values after the decimal point. 102 | Default is `None`. \n 103 | `emoji_spacing` (str, optional): Strategy for rendering emojis in tables. 104 | `mono` will emojis as single characters, suitable for monospaced fonts. 105 | `None` will not detect and process emojis. 106 | Default is `None`. \n 107 | `multiline` (Dict[str, int], optional): Renders the table with predefined widths by passing a dictionary with column names as keys and their respective widths as values. Note that the width of a column cannot be smaller than the longest contiguous string present in the data. 108 | Default is `None`. \n 109 | `multiline_strategy` (str, optional): Strategy applied to rendering contents in multiple lines. Possible values are: 110 | `rows`: Splits only rows overfilling the predefined column width. 111 | `header`: Splits only the header overfilling the predefined column width. 112 | `rows_and_header`: Splits rows and header overfilling the predefined column width. 113 | Default is `rows`. \n 114 | `multiline_delimiter` (str, optional): Character that will be used to split a cell's contents into multiple rows. 115 | Default is a blank space ` `. \n 116 | `quote` (bool, optional): Wraps the generated markdown table in block quotes ` ```table``` `. 117 | Default is `True`. 118 | 119 | Returns: 120 | self: Returns the instance with updated parameters. 121 | """ 122 | self.row_sep = row_sep 123 | self.padding_width = padding_width 124 | self.padding_weight = padding_weight 125 | self.padding_char = padding_char 126 | self.newline_char = newline_char 127 | self.float_rounding = float_rounding 128 | self.emoji_spacing = emoji_spacing 129 | self.multiline = multiline 130 | self.multiline_strategy = multiline_strategy 131 | self.multiline_delimiter = multiline_delimiter 132 | self.quote = quote 133 | 134 | if isinstance(padding_width, int): 135 | self.padding_width = {key: padding_width for key in self.data[0].keys()} 136 | if isinstance(padding_weight, str): 137 | self.padding_weight = {key: padding_weight for key in self.data[0].keys()} 138 | 139 | 140 | self.__validate_parameters() 141 | 142 | if not self.skip_data_validation: 143 | self.__validate_data(self.data) 144 | 145 | self.__update_meta_params() 146 | 147 | if self.multiline: 148 | self.__validate_multiline(self.data) 149 | 150 | return self 151 | 152 | def __update_meta_params(self): 153 | """Update and store internal meta-parameters""" 154 | if self.multiline: 155 | self.var_padding = self.multiline 156 | # add user-defined padding to the provided multiline column width dict 157 | for key, value in self.var_padding.items(): 158 | self.var_padding[key] = value + self.padding_width[key] 159 | else: 160 | self.var_padding = self.__get_padding() 161 | self.var_row_sep = self.__get_row_sep_str() 162 | # self.var_row_sep_last = self.__get_row_sep_last() 163 | self.var_row_sep_last = self.__get_row_sep_str() 164 | 165 | def __validate_parameters(self): # noqa: C901 166 | valid_values = { 167 | "row_sep": ["always", "topbottom", "markdown", None], 168 | "emoji_spacing": ["mono", None], 169 | "multiline_strategy": ["rows", "header", "rows_and_header"] 170 | } 171 | 172 | valid_dict_values = { 173 | "padding_weight": ["left", "right", "centerleft", "centerright"], 174 | } 175 | 176 | # Validate fixed value attributes 177 | for attr, values in valid_values.items(): 178 | if getattr(self, attr) not in values: 179 | raise ValueError(f"{attr} value of '{getattr(self, attr)}' is not valid. Possible values are {values}.") 180 | 181 | # Validate padding_weight 182 | if isinstance(self.padding_weight, dict): 183 | for key, value in self.padding_weight.items(): 184 | if value not in valid_dict_values["padding_weight"]: 185 | raise ValueError(f"padding_weight[{key}] value of '{value}' is not valid. Possible values are {valid_dict_values['padding_weight']}.") 186 | else: 187 | raise ValueError(f"padding_weight value of '{self.padding_weight}' is not valid.") 188 | 189 | # Validate padding_width 190 | if isinstance(self.padding_width, dict): 191 | for key, value in self.padding_width.items(): 192 | if not isinstance(value, int) or not (0 <= value < 100000): 193 | raise ValueError(f"padding_width[{key}] value of '{value}' is not valid. Possible range is 0 <= value < 100000.") 194 | else: 195 | raise ValueError(f"padding_width value of '{self.padding_width}' is not valid.") 196 | 197 | # Validate padding_char 198 | if not isinstance(self.padding_char, (str, dict)) or (isinstance(self.padding_char, str) and len(self.padding_char) != 1): 199 | raise ValueError(f"padding_char value of '{self.padding_char}' is not valid. Please use a single character string.") 200 | 201 | # Validate float_rounding 202 | if not isinstance(self.float_rounding, (type(None), int)): 203 | raise ValueError(f"float_rounding value of '{self.float_rounding}' is not valid. Please use an integer or leave as None.") 204 | 205 | # Validate multiline 206 | if not isinstance(self.multiline, (type(None), dict)): 207 | raise ValueError(f"multiline value of '{self.multiline}' is not valid. Please use a dict or leave as None.") 208 | 209 | # Validate multiline_delimiter 210 | if not isinstance(self.multiline_delimiter, str) or len(self.multiline_delimiter) != 1: 211 | raise ValueError(f"multiline_delimiter value of '{self.multiline_delimiter}' is not valid. Please use a single character string.") 212 | 213 | # Validate quote 214 | if not isinstance(self.quote, bool): 215 | raise ValueError(f"quote value of '{self.quote}' is not valid. Please use a boolean.") 216 | 217 | def __validate_data(self, data): 218 | # Check if all dictionaries in self.data have uniform keys 219 | keys = set(data[0].keys()) 220 | for item in data: 221 | if not isinstance(item, dict): 222 | raise TypeError("Each element in data must be a dictionary.") 223 | if set(item.keys()) != keys: 224 | raise ValueError("Dictionary keys are not uniform across data variable.") 225 | 226 | def __validate_multiline(self, data): 227 | for i, row in enumerate(data): 228 | for key in row.keys(): 229 | if key in self.var_padding: 230 | multiline_data = row[key].split(self.multiline_delimiter) 231 | multiline_max_string = max(multiline_data, key=len) 232 | multiline_max_width = len(multiline_max_string) 233 | if multiline_max_width + self.padding_width[key] > self.var_padding[key]: 234 | raise ValueError( 235 | f"There is a contiguous string:\n" 236 | f"'{multiline_max_string}'\n" 237 | f"in the element [{i}] " 238 | f"which is longer than the allocated column width " 239 | f"for column '{key}' and padding_width '{self.padding_width[key]}'." 240 | ) 241 | else: 242 | raise KeyError(f"Key '{key}' not found in var_padding.") 243 | 244 | def __get_padding(self): 245 | """Calculate table-wide padding.""" 246 | padding = {} 247 | for item in self.data[0].keys(): 248 | padding[item] = len(item) 249 | for item in self.data: 250 | for key in item.keys(): 251 | if self.float_rounding and isinstance(item[key], float): 252 | item[key] = round(item[key], self.float_rounding) 253 | # prepend float pre-processing 254 | if (padding[key] - self.padding_width[key]) < len(str(item[key])): 255 | padding[key] = len(str(item[key])) + self.padding_width[key] 256 | # prepend emoji pre-processing 257 | emoji = [] 258 | if self.emoji_spacing == "mono": 259 | emoji = count_emojis(item[key]) 260 | # adapt padding with all information 261 | if padding[key] - self.padding_width[key] - len(emoji) < len(str(item[key])): 262 | padding[key] = len(str(item[key])) + self.padding_width[key] + len(emoji) 263 | return padding 264 | 265 | def __get_row_sep_str(self): 266 | row_sep_str = "" 267 | for value in self.var_padding.values(): 268 | row_sep_str += "+" + "-" * value 269 | row_sep_str += "+" 270 | return row_sep_str 271 | 272 | def __get_margin(self, margin, key): 273 | # get column-specific alignment based on the column key (header) 274 | if self.padding_weight[key] == "left": 275 | right = 0 276 | elif self.padding_weight[key] == "right": 277 | right = margin 278 | elif self.padding_weight[key] == "centerleft": 279 | right = math.floor(margin / 2) 280 | elif self.padding_weight[key] == "centerright": 281 | right = math.ceil(margin / 2) 282 | else: 283 | right = math.floor(margin / 2) 284 | return right 285 | 286 | def __get_row(self, item): 287 | # checking if multiline variable for rows is set 288 | if self.multiline and self.multiline_strategy in ["rows", "rows_and_header"]: 289 | # local check if row needs to be split in multiple lines 290 | multiline = False 291 | for key in self.data[0].keys(): 292 | if len(item[key]) > self.var_padding[key]: 293 | multiline = True 294 | if "\n" in item[key]: 295 | multiline = True 296 | 297 | if multiline: 298 | return self.__get_multiline_row(item) 299 | return self.__get_normal_row(item) 300 | # if multiline is not set it's not multiline and return regular row 301 | return self.__get_normal_row(item) 302 | 303 | def __get_normal_row(self, item): 304 | row = "" 305 | for key in self.data[0].keys(): 306 | # preprend emoji pre-processing for cell values 307 | emoji = [] 308 | if self.emoji_spacing == "mono": 309 | emoji = count_emojis(item[key]) 310 | # extract column padding to local variable so that if emojis are present 311 | # the cell can be rendered with the extra spacing needed 312 | local_padding = self.var_padding[key] - len(emoji) 313 | margin = local_padding - len(str(item[key])) 314 | right = self.__get_margin(margin, key) 315 | row += "|" + str(item[key]).rjust( 316 | local_padding - right, self.padding_char 317 | ).ljust(local_padding, self.padding_char) 318 | row += "|" 319 | return row 320 | 321 | def __get_multiline_row(self, item): # noqa: C901 322 | multiline_items = {} 323 | 324 | # Helper function to process each element and split by emojis if present 325 | def split_and_process_element(element): 326 | emojis = count_emojis(element) 327 | if not emojis: 328 | return [element] 329 | emoji_indices = [emoji["index"] for emoji in emojis if "index" in emoji] 330 | return split_list_by_indices(element, emoji_indices) 331 | 332 | # Process each column in the row 333 | for key in self.data[0].keys(): 334 | multiline_row = [] 335 | # First we split by embedded line breaks in order to correctly 336 | # render lists and othe markdown elements which depend on newline offset 337 | for line in item[key].split("\n"): 338 | fully_split_cell = [] 339 | # Split cell content by the delimiter and process each part 340 | for element in line.split(self.multiline_delimiter): 341 | fully_split_cell.extend(split_and_process_element(element)) 342 | 343 | single_row = [] 344 | item_prev_length, spacing_between_items = 0, 0 345 | 346 | # Create multiline rows from the split elements 347 | while fully_split_cell: 348 | current_element = fully_split_cell[0] 349 | item_length = len(current_element) + len(count_emojis(current_element)) 350 | 351 | # Check if the current element fits in the row 352 | if item_length + item_prev_length + spacing_between_items + self.padding_width[key] <= self.var_padding[key]: 353 | item_prev_length += item_length 354 | single_row.append(fully_split_cell.pop(0)) 355 | spacing_between_items = len(single_row) 356 | else: 357 | # Start a new line if the current element doesn't fit 358 | multiline_row.append(" ".join(single_row)) 359 | single_row, item_prev_length, spacing_between_items = [], 0, 0 360 | 361 | # Add the remaining elements in single_row to multiline_row 362 | multiline_row.append(" ".join(single_row)) 363 | multiline_items[key] = multiline_row 364 | 365 | # Find the maximum number of rows in any column 366 | multiline_rows_max = max(map(len, multiline_items.values())) 367 | 368 | # Pad columns with fewer rows to ensure all columns have the same number of rows 369 | for key, value in multiline_items.items(): 370 | value.extend([self.padding_char * self.var_padding[key]] * (multiline_rows_max - len(value))) 371 | 372 | rows = "" 373 | # Create the final output by combining rows from each column 374 | for i in range(multiline_rows_max): 375 | row_dict = {key: multiline_items[key][i] for key in self.data[0].keys()} 376 | rows += self.__get_normal_row(row_dict) 377 | if i < multiline_rows_max - 1: 378 | rows += self.newline_char 379 | return rows 380 | 381 | 382 | def get_header(self): 383 | """Get the header of the markdown table""" 384 | header = "" 385 | if self.row_sep in ["topbottom", "always"]: 386 | header += self.newline_char + self.var_row_sep_last + self.newline_char 387 | 388 | # if header is set to be multirow 389 | if self.multiline and self.multiline_strategy in ["header", "rows_and_header"]: 390 | # invert keys with values, so that we can reuse the multiline row function 391 | inv_data = {k: k for k, _ in self.data[0].items()} 392 | header += self.__get_multiline_row(inv_data) 393 | header += self.newline_char 394 | # else header is not rendered as multiple rows 395 | else: 396 | for key in self.data[0].keys(): 397 | margin = self.var_padding[key] - len(key) 398 | right = self.__get_margin(margin, key) 399 | header += "|" + key.rjust( 400 | self.var_padding[key] - right, self.padding_char 401 | ).ljust(self.var_padding[key], self.padding_char) 402 | header += "|" + self.newline_char 403 | 404 | if self.row_sep == "always": 405 | header += self.var_row_sep + self.newline_char 406 | if self.row_sep == "markdown": 407 | header += self.var_row_sep.replace("+", "|") + self.newline_char 408 | return header 409 | 410 | def get_body(self): 411 | """Get the body of the markdown table""" 412 | rows = "" 413 | for i, item in enumerate(self.data): 414 | rows += self.__get_row(item) 415 | if i < len(self.data) - 1: 416 | rows += self.newline_char 417 | if self.row_sep == "always" and i < len(self.data) - 1: 418 | rows += self.var_row_sep + self.newline_char 419 | if self.row_sep in ["topbottom", "always"] and i == len(self.data) - 1: 420 | rows += self.newline_char + self.var_row_sep_last 421 | return rows 422 | 423 | def get_markdown(self): 424 | """Get the complete markdown table""" 425 | self.__update_meta_params() 426 | data = self.get_header() + self.get_body() 427 | if self.quote: 428 | return "```" + data + "```" 429 | return data -------------------------------------------------------------------------------- /py_markdown_table/utils.py: -------------------------------------------------------------------------------- 1 | """Util functions which may be used outside of the class for convenience""" 2 | from typing import List, Dict 3 | 4 | def count_emojis(text: str) -> List[Dict]: 5 | """Count emojis in a given string and return a list of emojis with index, value, and spacing.""" 6 | 7 | emoji_ranges = [ 8 | (0x1F600, 0x1F64F), # Emoticons 9 | (0x1F300, 0x1F5FF), # Miscellaneous Symbols and Pictographs 10 | (0x1F680, 0x1F6FF), # Transport and Map Symbols 11 | (0x2600, 0x26FF), # Miscellaneous Symbols 12 | (0x2700, 0x27BF), # Dingbats 13 | (0xFE00, 0xFE0F), # Variation Selectors 14 | (0x1F900, 0x1F9FF), # Supplemental Symbols and Pictographs 15 | (0x1F1E6, 0x1F1FF), # Flags (iOS) 16 | (0x1F7E0, 0x1F7FF), # Geometric Shapes Extended (🔴🟢) 17 | (0x2B00, 0x2BFF), # Additional geometric symbols (⬛⬜) 18 | (0x2190, 0x21FF), # Arrows (➡️ ⬆️ ⬇️ ⬅️) 19 | (0x1FA70, 0x1FAFF), # Extended symbols (🛗 🛻 🪐 🪑) 20 | ] 21 | 22 | emojis = [] 23 | 24 | for i, char in enumerate(text): 25 | code_point = ord(char) 26 | 27 | # Check if character is in an emoji range 28 | if any(start <= code_point <= end for start, end in emoji_ranges): 29 | emojis.append({"index": i, "value": char, "spacing": 2}) 30 | 31 | # Handle surrogate pairs 32 | if ( 33 | i + 1 < len(text) 34 | and 0xD800 <= code_point <= 0xDBFF 35 | and 0xDC00 <= ord(text[i + 1]) <= 0xDFFF 36 | ): 37 | full_code_point = 0x10000 + (code_point - 0xD800) * 0x400 + (ord(text[i + 1]) - 0xDC00) 38 | if any(start <= full_code_point <= end for start, end in emoji_ranges): 39 | emojis.append({"index": i, "value": char + text[i + 1], "spacing": 2}) 40 | 41 | # Handle keycap sequences (1️⃣ 2️⃣ 3️⃣ #️⃣) 42 | if i + 1 < len(text) and ord(text[i + 1]) == 0x20E3: 43 | emojis.append({"index": i, "value": char + text[i + 1], "spacing": 2}) 44 | 45 | return emojis 46 | 47 | 48 | def find_longest_contiguous_strings( 49 | data: List[Dict], include_header: bool = False, delimiter: str = " " 50 | ) -> Dict: 51 | """Finds the longest contiguous strings in a list of dicts. 52 | 53 | Args: 54 | data (List[Dict]): List of dicts containing the data to be rendered in a markdown table. 55 | See markdown_table object description. 56 | include_header (bool, optional): True if also headers should be parsed. Defaults to False. 57 | delimiter (str, optional): Which delimiter character to use when splitting a cell's contents. 58 | Defaults to " ". 59 | 60 | Returns: 61 | Dict: Dictionary containing the minimal width of each column for the input data with keys 62 | corresponding to the table headers 63 | """ 64 | longest_strings = {} 65 | if include_header: 66 | longest_strings = {key: len(key) for key in data[0].keys()} 67 | for dictionary in data: # pylint: disable=R1702 68 | for key, value in dictionary.items(): 69 | if isinstance(value, str): 70 | max_length = 0 71 | current_length = 0 72 | for char in value: 73 | if char != delimiter: 74 | current_length += 1 75 | if current_length > max_length: 76 | max_length = current_length 77 | else: 78 | current_length = 0 79 | if max_length > longest_strings.get(key, 0): 80 | longest_strings[key] = max_length 81 | return longest_strings 82 | 83 | 84 | def split_list_by_indices(lst: List, indices: List) -> List: 85 | """Used to split emojis into separate elements for the multirow rendering 86 | 87 | Args: 88 | lst (List): Input list which needs to be split 89 | indices (List): List of indices to split by 90 | 91 | Returns: 92 | List: List split by indices 93 | """ 94 | 95 | split_list = [] 96 | start = 0 97 | for index in indices: 98 | split_list.append(lst[start:index]) 99 | start = index 100 | split_list.append(lst[start:]) 101 | return split_list 102 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py-markdown-table" 3 | version = "1.3.0" 4 | description = "Package that generates markdown tables from a list of dicts" 5 | readme = "README.md" 6 | homepage = "https://github.com/hvalev/py-markdown-table" 7 | license = "MIT" 8 | authors = ["hvalev"] 9 | classifiers = [ 10 | "Programming Language :: Python :: 3", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | ] 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.8" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | ruff = "^0.9" 20 | pytest = "^8.0" 21 | pytest-cov = "^5.0" 22 | 23 | [tool.ruff] 24 | lint.select = ["E", "F", "C", "N", "Q", "B"] 25 | lint.ignore = ["E501"] 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /res/table_w_emoji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hvalev/py-markdown-table/3e464cb79abbe1063745fa4f71ae65371ab36bbd/res/table_w_emoji.jpg -------------------------------------------------------------------------------- /tests/test_markdown_table.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from py_markdown_table.markdown_table import markdown_table 3 | 4 | bad_data_0 = [] 5 | 6 | bad_data_1 = [{"one": "two", "three": "four"}, {"one": "two"}] 7 | 8 | bad_data_2 = [{"one": "two", "three": "four"}, {"one": "two", "four": "three"}] 9 | 10 | formatting_data = [ 11 | { 12 | "title": "Vrij Zwemmen", 13 | "time": "21:30-23:00", 14 | "date": "Wed 09.12", 15 | "seats": "24/24", 16 | }, 17 | { 18 | "title": "Vrij Zwemmen", 19 | "time": "12:00-13:00", 20 | "date": "Thu 10.12", 21 | "seats": "18/18", 22 | }, 23 | { 24 | "title": "Vrij Zwemmen", 25 | "time": "7:30-8:30", 26 | "date": "Fri 11.12", 27 | "seats": "18/18", 28 | }, 29 | { 30 | "title": "Vrij Zwemmen", 31 | "time": "13:15-14:15", 32 | "date": "Sat 12.12", 33 | "seats": "18/18", 34 | }, 35 | ] 36 | 37 | multiline_data = [ 38 | {"A": "row1_A and additional stuff", "B": "row1_B", "C": "row1_C"}, 39 | {"A": "row2_A", "B": "row2_B and additional stuff", "C": "row2_C"}, 40 | {"A": "row3_A", "B": "row3_B", "C": "row3_C"}, 41 | ] 42 | 43 | multirow_header_data = [ 44 | {"those are multi rows": "yes they are"}, 45 | {"those are multi rows": "no they are not"}, 46 | ] 47 | 48 | emoji_data = [ 49 | {"title": "Vrij Zwemmen", "time": "21:30-23:00", "date": "😊", "seats": "24/24"}, 50 | { 51 | "title": "Vrij Zwemmen", 52 | "time": "12:00-13:00", 53 | "date": "Thu 10.12", 54 | "seats": "18/18", 55 | }, 56 | {"title": "Vrij Zwemmen", "time": "7:30-8:30", "date": "Fri 11.12", "seats": "😊🌍🎉"}, 57 | { 58 | "title": "Vrij Zwemmen", 59 | "time": "13:15-14:15", 60 | "date": "Sat 12.12", 61 | "seats": "20/20", 62 | }, 63 | ] 64 | 65 | emoji_multiline_data = [ 66 | { 67 | "title that is maybe too long": "Vrij Zwemmen", 68 | "time": "21:30-23:00", 69 | "date": "😊", 70 | "seats": "24/24", 71 | }, 72 | { 73 | "title that is maybe too long": "Vrij Zwemmen", 74 | "time": "12:00-13:00", 75 | "date": "Thu 10.12", 76 | "seats": "18/18", 77 | }, 78 | { 79 | "title that is maybe too long": "Vrij Zwemmen", 80 | "time": "7:30-8:30", 81 | "date": "Fri 11.12", 82 | "seats": "😊🌍🎉", 83 | }, 84 | { 85 | "title that is maybe too long": "Vrij Zwemmen", 86 | "time": "13:15-14:15", 87 | "date": "Sat 12.12", 88 | "seats": "20/20", 89 | }, 90 | { 91 | "title that is maybe too long": "Vrij Zwemmen", 92 | "time": "7:30-8:30", 93 | "date": "Fri 11.12", 94 | "seats": " asd 😊-🌍:🎉", 95 | }, 96 | { 97 | "title that is maybe too long": "Zwemmen", 98 | "time": "13:15-14:15", 99 | "date": "Sat 12.12", 100 | "seats": "20/20", 101 | }, 102 | ] 103 | 104 | 105 | @pytest.mark.parametrize("data", [ 106 | (bad_data_0, pytest.raises(ValueError)), 107 | (bad_data_1, pytest.raises(ValueError)), 108 | (bad_data_2, pytest.raises(ValueError)), 109 | ]) 110 | def test_bad_data(data): 111 | dataset, expected_exception = data 112 | with expected_exception: 113 | markdown_table(dataset).get_markdown() 114 | 115 | 116 | @pytest.mark.parametrize("params, expected_output", [ 117 | ({}, "```\n+------------+-----------+---------+-----+\n| title | time | date |seats|\n+------------+-----------+---------+-----+\n|Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24|\n+------------+-----------+---------+-----+\n|Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18|\n+------------+-----------+---------+-----+\n|Vrij Zwemmen| 7:30-8:30 |Fri 11.12|18/18|\n+------------+-----------+---------+-----+\n|Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18|\n+------------+-----------+---------+-----+```"), 118 | ({"row_sep": "topbottom"}, "```\n+------------+-----------+---------+-----+\n| title | time | date |seats|\n|Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24|\n|Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18|\n|Vrij Zwemmen| 7:30-8:30 |Fri 11.12|18/18|\n|Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18|\n+------------+-----------+---------+-----+```"), 119 | ({"row_sep": "markdown"}, "```| title | time | date |seats|\n|------------|-----------|---------|-----|\n|Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24|\n|Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18|\n|Vrij Zwemmen| 7:30-8:30 |Fri 11.12|18/18|\n|Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18|```"), 120 | ({"row_sep": "markdown", "quote": False}, "| title | time | date |seats|\n|------------|-----------|---------|-----|\n|Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24|\n|Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18|\n|Vrij Zwemmen| 7:30-8:30 |Fri 11.12|18/18|\n|Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18|"), 121 | ({"row_sep": "topbottom", "padding_weight": "right"}, "```\n+------------+-----------+---------+-----+\n|title |time |date |seats|\n|Vrij Zwemmen|21:30-23:00|Wed 09.12|24/24|\n|Vrij Zwemmen|12:00-13:00|Thu 10.12|18/18|\n|Vrij Zwemmen|7:30-8:30 |Fri 11.12|18/18|\n|Vrij Zwemmen|13:15-14:15|Sat 12.12|18/18|\n+------------+-----------+---------+-----+```"), 122 | ({"row_sep": "topbottom", "padding_width": 5, "padding_weight": "left"}, "```\n+-----------------+----------------+--------------+----------+\n| title| time| date| seats|\n| Vrij Zwemmen| 21:30-23:00| Wed 09.12| 24/24|\n| Vrij Zwemmen| 12:00-13:00| Thu 10.12| 18/18|\n| Vrij Zwemmen| 7:30-8:30| Fri 11.12| 18/18|\n| Vrij Zwemmen| 13:15-14:15| Sat 12.12| 18/18|\n+-----------------+----------------+--------------+----------+```"), 123 | ({"row_sep": "topbottom", "padding_width": 5, "padding_weight": "centerright"}, "```\n+-----------------+----------------+--------------+----------+\n| title | time | date | seats |\n| Vrij Zwemmen | 21:30-23:00 | Wed 09.12 | 24/24 |\n| Vrij Zwemmen | 12:00-13:00 | Thu 10.12 | 18/18 |\n| Vrij Zwemmen | 7:30-8:30 | Fri 11.12 | 18/18 |\n| Vrij Zwemmen | 13:15-14:15 | Sat 12.12 | 18/18 |\n+-----------------+----------------+--------------+----------+```"), 124 | ({"row_sep": "always", "padding_width": 5, "padding_weight": "centerright", "padding_char": "."}, "```\n+-----------------+----------------+--------------+----------+\n|......title......|......time......|.....date.....|..seats...|\n+-----------------+----------------+--------------+----------+\n|..Vrij Zwemmen...|..21:30-23:00...|..Wed 09.12...|..24/24...|\n+-----------------+----------------+--------------+----------+\n|..Vrij Zwemmen...|..12:00-13:00...|..Thu 10.12...|..18/18...|\n+-----------------+----------------+--------------+----------+\n|..Vrij Zwemmen...|...7:30-8:30....|..Fri 11.12...|..18/18...|\n+-----------------+----------------+--------------+----------+\n|..Vrij Zwemmen...|..13:15-14:15...|..Sat 12.12...|..18/18...|\n+-----------------+----------------+--------------+----------+```"), 125 | ({"row_sep": "always", "padding_width": {"title": 2, "time": 4, "date": 3, "seats": 1}, "padding_weight": {"title": "left", "time": "right", "date": "centerleft", "seats": "centerright"}}, "```\n+--------------+---------------+------------+------+\n| title|time | date |seats |\n+--------------+---------------+------------+------+\n| Vrij Zwemmen|21:30-23:00 | Wed 09.12 |24/24 |\n+--------------+---------------+------------+------+\n| Vrij Zwemmen|12:00-13:00 | Thu 10.12 |18/18 |\n+--------------+---------------+------------+------+\n| Vrij Zwemmen|7:30-8:30 | Fri 11.12 |18/18 |\n+--------------+---------------+------------+------+\n| Vrij Zwemmen|13:15-14:15 | Sat 12.12 |18/18 |\n+--------------+---------------+------------+------+```"), 126 | ]) 127 | def test_formatting(params, expected_output): 128 | mt = markdown_table(formatting_data).set_params(**params).get_markdown() 129 | assert mt == expected_output 130 | 131 | 132 | @pytest.mark.parametrize("params, expected_output", [ 133 | ({"padding_width": 0, "padding_weight": "centerleft", "multiline": {"A": 25, "B": 12, "C": 9}}, "```\n+-------------------------+------------+---------+\n| A | B | C |\n+-------------------------+------------+---------+\n| row1_A and additional | row1_B | row1_C |\n| stuff | | |\n+-------------------------+------------+---------+\n| row2_A | row2_B and | row2_C |\n| | additional | |\n| | stuff | |\n+-------------------------+------------+---------+\n| row3_A | row3_B | row3_C |\n+-------------------------+------------+---------+```"), 134 | ({"padding_width": 2, "padding_weight": "centerleft", "multiline": {"A": 25, "B": 12, "C": 9}}, "```\n+-----------------------------+----------------+-------------+\n| A | B | C |\n+-----------------------------+----------------+-------------+\n| row1_A and additional stuff | row1_B | row1_C |\n+-----------------------------+----------------+-------------+\n| row2_A | row2_B and | row2_C |\n| | additional | |\n| | stuff | |\n+-----------------------------+----------------+-------------+\n| row3_A | row3_B | row3_C |\n+-----------------------------+----------------+-------------+```"), 135 | ({"row_sep": "always", "padding_width": {"A": 2, "B": 4, "C": 3}, "padding_weight": {"A": "left", "B": "right", "C": "centerleft"}}, "```\n+-----------------------------+-------------------------------+---------+\n| A|B | C |\n+-----------------------------+-------------------------------+---------+\n| row1_A and additional stuff|row1_B | row1_C |\n+-----------------------------+-------------------------------+---------+\n| row2_A|row2_B and additional stuff | row2_C |\n+-----------------------------+-------------------------------+---------+\n| row3_A|row3_B | row3_C |\n+-----------------------------+-------------------------------+---------+```"), 136 | ]) 137 | def test_multiline_data(params, expected_output): 138 | mt = markdown_table(multiline_data).set_params(**params).get_markdown() 139 | assert mt == expected_output 140 | 141 | 142 | @pytest.mark.parametrize("params, expected_output", [ 143 | ({"row_sep": "always", "multiline": {"those are multi rows": 5}, "multiline_strategy": "rows_and_header"}, "```\n+-----+\n|those|\n| are |\n|multi|\n| rows|\n+-----+\n| yes |\n| they|\n| are |\n+-----+\n| no |\n| they|\n| are |\n| not |\n+-----+```"), 144 | ]) 145 | def test_multirow_header_data(params, expected_output): 146 | mt = markdown_table(multirow_header_data).set_params(**params).get_markdown() 147 | assert mt == expected_output 148 | 149 | 150 | @pytest.mark.parametrize("params, expected_output", [ 151 | ({"row_sep": "topbottom", "emoji_spacing": "mono"}, "```\n+------------+-----------+---------+------+\n| title | time | date | seats|\n|Vrij Zwemmen|21:30-23:00| 😊 | 24/24|\n|Vrij Zwemmen|12:00-13:00|Thu 10.12| 18/18|\n|Vrij Zwemmen| 7:30-8:30 |Fri 11.12|😊🌍🎉|\n|Vrij Zwemmen|13:15-14:15|Sat 12.12| 20/20|\n+------------+-----------+---------+------+```"), 152 | ]) 153 | def test_emoji_data(params, expected_output): 154 | mt = markdown_table(emoji_data).set_params(**params).get_markdown() 155 | assert mt == expected_output 156 | 157 | 158 | @pytest.mark.parametrize("params, expected_output", [ 159 | ({"row_sep": "topbottom", "emoji_spacing": "mono", "multiline": {"title that is maybe too long": 7, "time": 11, "date": 5, "seats": 5}, "multiline_strategy": "rows_and_header"}, "```\n+-------+-----------+-----+-----+\n| title | time | date|seats|\n|that is| | | |\n| maybe | | | |\n| too | | | |\n| long | | | |\n| Vrij |21:30-23:00| 😊 |24/24|\n|Zwemmen| | | |\n| Vrij |12:00-13:00| Thu |18/18|\n|Zwemmen| |10.12| |\n| Vrij | 7:30-8:30 | Fri | 😊 |\n|Zwemmen| |11.12|🌍 🎉|\n| Vrij |13:15-14:15| Sat |20/20|\n|Zwemmen| |12.12| |\n| Vrij | 7:30-8:30 | Fri | asd |\n|Zwemmen| |11.12| 😊-|\n| | | | 🌍: |\n| | | | 🎉 |\n|Zwemmen|13:15-14:15| Sat |20/20|\n| | |12.12| |\n+-------+-----------+-----+-----+```"), 160 | ]) 161 | def test_emoji_multiline_data(params, expected_output): 162 | mt = markdown_table(emoji_multiline_data).set_params(**params).get_markdown() 163 | assert mt == expected_output 164 | 165 | -------------------------------------------------------------------------------- /utils/benchmark.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | import string 3 | import random 4 | import time 5 | from py_markdown_table.markdown_table import markdown_table 6 | from py_markdown_table.utils import find_longest_contiguous_strings 7 | 8 | 9 | def generate_dict_list(num_keys, num_dicts, values_length, include_whitespace=False): 10 | dict_list = [] 11 | keys = [f"key{i}" for i in range(1, num_keys + 1)] 12 | for _ in range(num_dicts): 13 | dictionary = {} 14 | for key in keys: 15 | value = "".join( 16 | random.choice(string.ascii_letters + string.digits + " ") 17 | for _ in range(values_length) 18 | ) 19 | dictionary[key] = value 20 | dict_list.append(dictionary) 21 | return dict_list 22 | 23 | 24 | # Define the parameter ranges 25 | num_keys_range = [2, 4, 8, 16] 26 | num_dicts_range = [10, 40, 160, 640] 27 | values_length_range = [5, 20, 80, 320] 28 | include_whitespace_range = [False, False, True, True] 29 | 30 | # num_keys_range = [3] 31 | # num_dicts_range = [2] 32 | # values_length_range = [40] 33 | # include_whitespace_range = [True] 34 | 35 | # Create a list to store the sampled parameter combinations 36 | parameter_combinations = [] 37 | 38 | # Iterate over the parameter ranges 39 | for i in range(4): 40 | num_keys = num_keys_range[i] 41 | num_dicts = num_dicts_range[i] 42 | values_length = values_length_range[i] 43 | include_whitespace = include_whitespace_range[i] 44 | 45 | # Generate the dictionary list for the current parameter combination 46 | dictionary_list = generate_dict_list( 47 | num_keys, num_dicts, values_length, include_whitespace 48 | ) 49 | 50 | # Store the parameter combination and the generated dictionary list 51 | parameter_combinations.append( 52 | { 53 | "num_keys": num_keys, 54 | "num_dicts": num_dicts, 55 | "values_length": values_length, 56 | "include_whitespace": include_whitespace, 57 | "dictionary_list": dictionary_list, 58 | } 59 | ) 60 | 61 | 62 | def create_benchmark(columns, rows, cell_length, multiline, speed): 63 | return { 64 | "parameters": f"columns: {columns}~rows: {rows}~cell_size: {cell_length}", 65 | "Multiline": str(multiline), 66 | "speed": f"{str('{:.6f}'.format(speed))} ms", 67 | } 68 | 69 | 70 | # find biggest contiguous string in the elements of a list of dicts 71 | results = [] 72 | 73 | print("########### without multiline ##############") 74 | # Print the sampled parameter combinations without multiline 75 | for params in parameter_combinations: 76 | print("Parameter Combination:") 77 | print(f"columns: {params['num_keys']}") 78 | print(f"rows: {params['num_dicts']}") 79 | print(f"cell_value_length: {params['values_length']}") 80 | # print(f"include_whitespace: {params['include_whitespace']}") 81 | # print(f"dictionary_list: {params['dictionary_list']}") 82 | 83 | start_time = time.time() * 1000 84 | markdown_table(params["dictionary_list"]).set_params( 85 | padding_width=0, padding_weight="centerleft" 86 | ).get_markdown() 87 | end_time = time.time() * 1000 88 | elapsed_time = end_time - start_time 89 | print(f"Benchmark - Elapsed Time: {elapsed_time} milliseconds") 90 | results.append( 91 | create_benchmark( 92 | params["num_keys"], 93 | params["num_dicts"], 94 | params["values_length"], 95 | False, 96 | elapsed_time, 97 | ) 98 | ) 99 | 100 | print("########### with multiline ##############") 101 | # Print the sampled parameter combinations with multiline 102 | for params in parameter_combinations: 103 | print("Parameter Combination:") 104 | print(f"columns: {params['num_keys']}") 105 | print(f"rows: {params['num_dicts']}") 106 | print(f"cell_value_length: {params['values_length']}") 107 | # print(f"include_whitespace: {params['include_whitespace']}") 108 | # print(f"dictionary_list: {params['dictionary_list']}") 109 | 110 | start_time = time.time() * 1000 111 | res = find_longest_contiguous_strings(params["dictionary_list"]) 112 | markdown_table(params["dictionary_list"]).set_params( 113 | padding_width=0, padding_weight="centerleft", multiline=res 114 | ).get_markdown() 115 | end_time = time.time() * 1000 116 | elapsed_time = end_time - start_time 117 | print(f"Benchmark - Elapsed Time: {elapsed_time} milliseconds") 118 | results.append( 119 | create_benchmark( 120 | params["num_keys"], 121 | params["num_dicts"], 122 | params["values_length"], 123 | True, 124 | elapsed_time, 125 | ) 126 | ) 127 | 128 | print(results) 129 | res = find_longest_contiguous_strings(results, delimiter="~") 130 | benchmark = ( 131 | markdown_table(results) 132 | .set_params( 133 | padding_width=2, 134 | padding_weight="centerleft", 135 | multiline=res, 136 | multiline_delimiter="~", 137 | ) 138 | .get_markdown() 139 | ) 140 | print(benchmark) 141 | -------------------------------------------------------------------------------- /utils/gendesc.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # flake8 noqa: E501 3 | from py_markdown_table.markdown_table import markdown_table 4 | 5 | # [monospaced fonts](https://en.wikipedia.org/wiki/Monospaced_font) 6 | params = [ 7 | { 8 | "param": "row_sep", 9 | "type": "str", 10 | "values": "", 11 | "description": "Row separation strategy using `----` as pattern", 12 | }, 13 | {"param": "", "type": "", "values": "always", "description": "Separate each row"}, 14 | { 15 | "param": "", 16 | "type": "", 17 | "values": "topbottom", 18 | "description": "Separate the top (header) and bottom (last row) of the table", 19 | }, 20 | { 21 | "param": "", 22 | "type": "", 23 | "values": "markdown", 24 | "description": "Separate only header from body", 25 | }, 26 | { 27 | "param": "", 28 | "type": "", 29 | "values": "None", 30 | "description": "No row separators will be inserted", 31 | }, 32 | { 33 | "param": "padding_width", 34 | "type": "int or dict", 35 | "values": "", 36 | "description": "Allocate padding to all table cells when passing an int or per-column when passing a dict", 37 | }, 38 | { 39 | "param": "padding_weight", 40 | "type": "str or dict", 41 | "values": "", 42 | "description": "Strategy for allocating padding within table cells. Per-column when passing a dict", 43 | }, 44 | { 45 | "param": "", 46 | "type": "", 47 | "values": "left", 48 | "description": "Aligns the cell's contents to the end of the cell", 49 | }, 50 | { 51 | "param": "", 52 | "type": "", 53 | "values": "right", 54 | "description": "Aligns the cell's contents to the beginning of the cell", 55 | }, 56 | { 57 | "param": "", 58 | "type": "", 59 | "values": "centerleft", 60 | "description": "Centers cell's contents with extra padding allocated to the beginning of the cell", 61 | }, 62 | { 63 | "param": "", 64 | "type": "", 65 | "values": "centerright", 66 | "description": "Centers cell's contents with extra padding allocated to the end of the cell", 67 | }, 68 | { 69 | "param": "padding_char", 70 | "type": "str", 71 | "values": "", 72 | "description": "Single character used to fill padding with. Default is a blank space ` `.", 73 | }, 74 | { 75 | "param": "newline_char", 76 | "type": "str", 77 | "values": "", 78 | "description": "Character appended to each row to force a newline. Default is `\\n`", 79 | }, 80 | { 81 | "param": "float_rounding", 82 | "type": "int", 83 | "values": "", 84 | "description": "Integer denoting the precision of cells of `floats` after the decimal point. Default is `None`.", 85 | }, 86 | { 87 | "param": "emoji_spacing", 88 | "type": "str", 89 | "values": "", 90 | "description": "Strategy for rendering emojis in tables. Currently only `mono` is supported for monospaced fonts. Default is `None` which disables special handling of emojis.", 91 | }, 92 | { 93 | "param": "multiline", 94 | "type": "dict", 95 | "values": "", 96 | "description": "Renders the table with predefined widths by passing a `dict` with `keys` being the column names (e.g. equivalent to those in the passed `data` variable) and `values` -- the `width` of each column as an integer. Note that the width of a column cannot be smaller than the longest contiguous string present in the data.", 97 | }, 98 | { 99 | "param": "multiline_strategy", 100 | "type": "str", 101 | "values": "", 102 | "description": "Strategy applied to rendering contents in multiple lines. Possible values are `rows`, `header` or `rows_and_header`. The default value is `rows`.", 103 | }, 104 | { 105 | "param": "", 106 | "type": "", 107 | "values": "rows", 108 | "description": "Splits only rows overfilling by the predefined column width as provided in the `multiline` variable", 109 | }, 110 | { 111 | "param": "", 112 | "type": "", 113 | "values": "header", 114 | "description": "Splits only the header overfilling by the predefined column width as provided in the `multiline` variable", 115 | }, 116 | { 117 | "param": "", 118 | "type": "", 119 | "values": "rows_and_header", 120 | "description": "Splits rows and header overfilling by the predefined column width as provided in the `multiline` variable", 121 | }, 122 | { 123 | "param": "multiline_delimiter", 124 | "type": "str", 125 | "values": "", 126 | "description": "Character that will be used to split a cell's contents into multiple rows. The default value is a blank space ` `.", 127 | }, 128 | { 129 | "param": "quote", 130 | "type": "bool", 131 | "values": "", 132 | "description": "Wraps the generated markdown table in block quotes ```table```. Default is `True`.", 133 | }, 134 | ] 135 | 136 | 137 | widths = {"param": 19, "type": 17, "values": 15, "description": 28} 138 | 139 | print( 140 | markdown_table(params) 141 | .set_params( 142 | padding_char=" ", 143 | padding_weight="centerleft", 144 | padding_width=2, 145 | row_sep="always", 146 | multiline=widths, 147 | ) 148 | .get_markdown() 149 | ) 150 | --------------------------------------------------------------------------------