PrettyTables.jl breaks on non-breaking release

Hello,

I use PrettyTables.jl to print the contents of a struct to the REPL. Sometime between 2.0 and 2.3.1 it stopped working. Here is an example showing it works with 2.0:

Code

using SequentialSamplingModels
dist = WaldMixture(;ν=2.0, η=1.0, α=.5, τ=.130)

Output

julia> dist
WaldMixture
┌───────────┬───────┐
│ Parameter │ Value │
├───────────┼───────┤
│ ν         │  2.00 │
│ η         │  1.00 │
│ α         │  0.50 │
│ τ         │  0.13 │
└───────────┴───────┘

Versions


(pretty_table_error) pkg> st
Status `~/.julia/dev/sandbox/pretty_table_error/Project.toml`
⌃ [08abe8d2] PrettyTables v2.0.0
  [0e71a2a6] SequentialSamplingModels v0.8.5
Info Packages marked with ⌃ have new versions available and may be upgradable.

Now if I upgrade to 2.3.1, the same example stops working:

Versions

(pretty_table_error) pkg> st
Status `~/.julia/dev/sandbox/pretty_table_error/Project.toml`
  [08abe8d2] PrettyTables v2.3.1
  [0e71a2a6] SequentialSamplingModels v0.8.5

Error

Summary
ERROR: MethodError: no method matching _print_table_with_text_back_end(::PrettyTables.PrintInfo; row_name_column_title::String, row_name_alignment::Symbol, row_names::Vector{Symbol})

Closest candidates are:
  _print_table_with_text_back_end(::PrettyTables.PrintInfo; alignment_anchor_fallback, alignment_anchor_fallback_override, alignment_anchor_regex, autowrap, body_hlines, body_hlines_format, continuation_row_alignment, crop, crop_subheader, columns_width, display_size, equal_columns_width, ellipsis_line_skip, highlighters, hlines, linebreaks, maximum_columns_width, minimum_columns_width, newline_at_end, overwrite, reserved_display_lines, show_omitted_cell_summary, sortkeys, tf, title_autowrap, title_same_width_as_table, vcrop_mode, vlines, border_crayon, header_crayon, omitted_cell_summary_crayon, row_label_crayon, row_label_header_crayon, row_number_header_crayon, subheader_crayon, text_crayon, title_crayon, T, T) got unsupported keyword arguments "row_name_column_title", "row_name_alignment", "row_names"
   @ PrettyTables ~/.julia/packages/PrettyTables/E8rPJ/src/backends/text/text_backend.jl:11

Stacktrace:
  [1] kwerr(::NamedTuple{(:row_name_column_title, :row_name_alignment, :row_names), Tuple{String, Symbol, Vector{Symbol}}}, ::Function, ::PrettyTables.PrintInfo)
    @ Base ./error.jl:165
  [2] _print_table(io::IO, data::Any; alignment::Symbol, backend::Val{:auto}, cell_alignment::Nothing, cell_first_line_only::Bool, compact_printing::Bool, formatters::PrettyTables.var"#36#39"{Printf.Format{Base.CodeUnits{UInt8, String}, Tuple{Printf.Spec{Val{'f'}}}}}, header::Tuple{Vector{String}}, header_alignment::Symbol, header_cell_alignment::Nothing, limit_printing::Bool, max_num_of_columns::Int64, max_num_of_rows::Int64, renderer::Symbol, row_labels::Nothing, row_label_alignment::Symbol, row_label_column_title::String, row_number_alignment::Symbol, row_number_column_title::String, show_header::Bool, show_row_number::Bool, show_subheader::Bool, title::String, title_alignment::Symbol, kwargs::Base.Pairs{Symbol, Any, Tuple{Symbol, Symbol, Symbol}, NamedTuple{(:row_name_column_title, :row_name_alignment, :row_names), Tuple{String, Symbol, Vector{Symbol}}}})
    @ PrettyTables ~/.julia/packages/PrettyTables/E8rPJ/src/print.jl:1005
  [3] _print_table
    @ ~/.julia/packages/PrettyTables/E8rPJ/src/print.jl:880 [inlined]
  [4] pretty_table(io::IO, data::Any; header::Vector{String}, kwargs::Base.Pairs{Symbol, Any, NTuple{7, Symbol}, NamedTuple{(:title, :row_name_column_title, :compact_printing, :row_name_alignment, :row_names, :formatters, :alignment), Tuple{String, String, Bool, Symbol, Vector{Symbol}, PrettyTables.var"#36#39"{Printf.Format{Base.CodeUnits{UInt8, String}, Tuple{Printf.Spec{Val{'f'}}}}}, Symbol}}})
    @ PrettyTables ~/.julia/packages/PrettyTables/E8rPJ/src/print.jl:771
  [5] _show(io::IOContext{Base.TTY}, model::WaldMixture{Float64})
    @ SequentialSamplingModels ~/.julia/packages/SequentialSamplingModels/UYgrK/src/utilities.jl:18
  [6] show(io::IOContext{Base.TTY}, #unused#::MIME{Symbol("text/plain")}, model::WaldMixture{Float64})
    @ SequentialSamplingModels ~/.julia/packages/SequentialSamplingModels/UYgrK/src/utilities.jl:2
  [7] (::REPL.var"#55#56"{REPL.REPLDisplay{REPL.LineEditREPL}, MIME{Symbol("text/plain")}, Base.RefValue{Any}})(io::Any)
    @ REPL ~/julia-1.9.4/share/julia/stdlib/v1.9/REPL/src/REPL.jl:276
  [8] with_repl_linfo(f::Any, repl::REPL.LineEditREPL)
    @ REPL ~/julia-1.9.4/share/julia/stdlib/v1.9/REPL/src/REPL.jl:557
  [9] display(d::REPL.REPLDisplay, mime::MIME{Symbol("text/plain")}, x::Any)
    @ REPL ~/julia-1.9.4/share/julia/stdlib/v1.9/REPL/src/REPL.jl:262
 [10] display(d::REPL.REPLDisplay, x::Any)
    @ REPL ~/julia-1.9.4/share/julia/stdlib/v1.9/REPL/src/REPL.jl:281
 [11] display(x::Any)
    @ Base.Multimedia ./multimedia.jl:340
 [12] #invokelatest#2
    @ ./essentials.jl:819 [inlined]
 [13] invokelatest
    @ ./essentials.jl:816 [inlined]
 [14] (::VSCodeServer.var"#67#72"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/eval.jl:229
 [15] withpath(f::VSCodeServer.var"#67#72"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams}, path::String)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/repl.jl:274
 [16] (::VSCodeServer.var"#66#71"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/eval.jl:179
 [17] hideprompt(f::VSCodeServer.var"#66#71"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/repl.jl:38
 [18] (::VSCodeServer.var"#65#70"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/eval.jl:150
 [19] with_logstate(f::Function, logstate::Any)
    @ Base.CoreLogging ./logging.jl:514
 [20] with_logger
    @ ./logging.jl:626 [inlined]
 [21] (::VSCodeServer.var"#64#69"{VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/eval.jl:255
 [22] #invokelatest#2
    @ ./essentials.jl:819 [inlined]
 [23] invokelatest(::Any)
    @ Base ./essentials.jl:816
 [24] macro expansion
    @ ~/.vscode/extensions/julialang.language-julia-1.60.2/scripts/packages/VSCodeServer/src/eval.jl:34 [inlined]
 [25] (::VSCodeServer.var"#62#63")()
    @ VSCodeServer ./task.jl:514

Does anyone know what is causing the error and how to fix it? In addition, should the version reflect the breaking behavior?

I think row_name* keyword parameters changed to row_label*

Thank you. It looks like the keyword is row_labels (pluralized), row_name_column_titlerow_label_column_title, and row_name_alignmentrow_label_alignment.

I opened an issue and the author will be releasing some fixes.

2 Likes

There was a change. At first I though @ronins_BR wasn’t following semver, or was sloppy (EDIT: actually semver was violated), but I think this is allowed. And he’s doing excellent job following the rules, not doing more than asked, also you can argue with the rules…:

Note, 2.0 was released with breaking changes, clearly marked in the CHANGELOG, e.g.:

  • BREAKING The default option of standalone in HTML backend is new false instead of true.
    [probably a typo there, new-> now.]
  • Deprecation The following options and structures were renamed (the old versions are now deprecated):
  • HTMLDecoration => HtmlDecoration
  • […]
  • `row_name* => row_label

Note, it doesn’t say breaking for the change since it was still usable (but you should stop using, migrate to the new).

It’s easy to just keep using and not even know of this since julia --depwarn=yes (or error) is no longer the default. So when it eventually gets dropped it may come as a surprise. I think semver allows dropping deprecated in any later minor version, not just major. Would it have been better to state it, or wait until 3.0? Maybe…

Or just never drop stuff. That’s what Rick Hickey suggests doing, argues against semver. There’s never a strict need to drop stuff rather than keep synonyms. It may clutter your codebase a bit, could be put into a separate file, disk space is cheap, I would worry more about startup cost, but synonyms could be NOT precompiled.

EDIT: Should have waited until next major to drop deprecated (according to semver rules, while Hickey would argue never):

How should I handle deprecating functionality?

Deprecating existing functionality is a normal part of software development and is often required to make forward progress. When you deprecate part of your public API, you should do two things: (1) update your documentation to let users know about the change, (2) issue a new minor release with the deprecation in place. Before you completely remove the functionality in a new major release there should be at least one minor release that contains the deprecation so that users can smoothly transition to the new API.

Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backward compatible functionality is introduced to the public API. It MUST be incremented if any public API functionality is marked as deprecated.

Thanks for clarifying about the warnings not showing by default. That explains why I never saw the deprecation warnings. Does that mean that the deprecation warning was removed sometime in 2.x, which created the appearance of a breaking change mid 2.x?

The only thing that doesn’t quite make sense is that under the release notes, I see * row_name* => row_label* for 2.0, but I don’t see the other keyword changes (there were three I had to make). Maybe the list was incomplete?

If you run with e.g. --depwarn=error then you would notice all the deprecated functions you actually try to use, from version 2.0 (i.e. since when deprecation added) on-wards.

It would be good to try such when you do a major upgrade, though note this wasn’t your fault, just that you could have avoided being affected.

Yes, when the deprecated function was removed in some 2.x, it was a breaking change (not strictly in 2.0), that was done by mistake. The warning (if you had enabled them or erroring) would have gone away in that 2.x, since the deprecation is attached to a function and it’s gone. There’s no way to keep it except by NOT removing the function, currently. It might be worthwhile for Julia to have a way of keeping just the warning (for a more helpful error message), but it would mean you would have to keep the function definition around in some form (though might skip the function body), if it’s deemed worthwhile. It seems though you might as well just not remove anything, also keep the body. The body can be rather trivial, just a call to its replacement function…

It was actually a bug to remove in the minor version.

1 Like

Thanks for your explanation. What happened now makes sense. I was not aware that deprecation warnings are turned off by default. In the future, I will turn on the warnings before upgrading to breaking versions of dependencies.